Skip to content

Testing: Refactor Slack router for dependency injection (mock-friendly) #19

@fbraza

Description

@fbraza

Goal

Make app/api/routers/slack_agent.py testable without real network calls by introducing small dependency-injection seams that tests can override. This enables reliable integration tests that mock Slack and the LLM model.

Why

  • The router currently constructs a real WebClient and Agent at import time, making tests slow and flaky and risking external calls.
  • With DI seams, tests can inject fakes/mocks; production behavior remains unchanged.

Acceptance Criteria

  • Add provider functions get_client() and get_agent() used via FastAPI Depends in the /slack route.
  • No real network calls during integration tests when providers are overridden.
  • Runtime behavior of the API remains unchanged when using defaults.
  • Clear examples in docstring/comments for how tests override dependencies.

Proposed Implementation

Refactor app/api/routers/slack_agent.py:

# app/api/routers/slack_agent.py
from pathlib import Path
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openrouter import OpenRouterProvider
from slack_sdk import WebClient

from app.tools import slack
from app.core.config import get_settings

prompt_path = Path(__file__).resolve().parents[2] / "pompts" / "slack.md"
prompt_instructions = prompt_path.read_text(encoding="utf-8")

router = APIRouter()

class Prompt(BaseModel):
    user_instruction: str

# Providers (override in tests)

def get_client() -> WebClient:
    settings = get_settings()
    return WebClient(token=settings.slack_token or "")


def get_agent() -> Agent:
    settings = get_settings()
    model = OpenAIChatModel(
        settings.model_name,
        provider=OpenRouterProvider(api_key=settings.open_router_token or ""),
    )
    return Agent(model=model, deps_type=slack.Deps, instructions=prompt_instructions)


@router.post("/slack", tags=["slack"])
def prompt_slack_agent(
    prompt: Prompt,
    client: WebClient = Depends(get_client),
    agent: Agent = Depends(get_agent),
):
    try:
        result = agent.run_sync(
            prompt.user_instruction,
            deps=slack.Deps(client=client),
            toolsets=[slack.slack_toolset],  # type: ignore
        )
        return {"content": result.output, "status": "ok"}
    except Exception as e:
        return {"msg": str(e), "status": "failed"}

Notes:

  • Keep the same prompt and toolset usage; only the construction shifts into providers.
  • Tests can override get_client and get_agent easily via app.dependency_overrides.

Test Override Examples

Override both dependencies in tests:

# tests/conftest.py (or inside the specific test)
from types import SimpleNamespace
from fastapi.testclient import TestClient
from app.api.routers import slack_agent as rtr

class FakeClient:
    def __init__(self):
        self.calls = []
    def chat_postMessage(self, channel: str, text: str):
        self.calls.append({"channel": channel, "text": text})
        return {"ok": True}

class FakeAgent:
    def __init__(self, output: str = "ok"):
        self.calls = []
        self.output = output
    def run_sync(self, instruction, deps, toolsets=None):
        self.calls.append({"instruction": instruction, "deps": deps, "toolsets": toolsets})
        # return shape compatible with pydantic-ai result
        return SimpleNamespace(output=f"stub:{self.output}")

fake_client = FakeClient()
fake_agent = FakeAgent()

# Apply overrides
from fastapi import FastAPI
app = FastAPI(title="testing API")
app.include_router(rtr.router)
app.dependency_overrides[rtr.get_client] = lambda: fake_client
app.dependency_overrides[rtr.get_agent] = lambda: fake_agent

client = TestClient(app)

Then, in a test:

def test_slack_endpoint_with_fakes(client):
    resp = client.post("/slack", json={"user_instruction": "say hi"})
    assert resp.status_code == 200
    assert resp.json()["status"] == "ok"

Alternatives Considered

  • Monkeypatch globals: monkeypatch.setattr(slack_agent, "client", FakeClient()) and monkeypatch.setattr(slack_agent, "agent", FakeAgent()). This works today without refactor, but is more brittle as it depends on module import order and hidden globals.
  • Adapter wrapper: create a thin SlackService class that wraps WebClient and inject that instead. Slightly more code; similar benefits.

Definition of Done

  • Code refactor merged with providers and route wired to Depends.
  • CI/tests pass, and integration tests can override dependencies to avoid network.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions