Testing an Extension
An Extension’s tests live in its repository’s tests/ directory, beside its package. They call
its tools directly, with the settings they need, without installing the Extension, without the
database, and without an LLM.
Testing a Tool
smarter.apps.llmtool.extension.testing has two helpers:
tool_context()returns a tool call’s context, with the settings you give it.call_tool()calls a tool as the platform does: it validates the arguments, and returns anExtensionToolError, or invalid arguments, as{"error": ...}. Unlike the platform, it lets any other exception propagate, so that your test sees it.
Mock the third-party services, so that the tests are fast and need no API keys:
"""Tests of the get_weather tool."""
import unittest
from unittest.mock import patch
from pydantic import SecretStr
from smarter.apps.llmtool.extension.testing import call_tool, tool_context
from weather.models import Conditions
from weather.services import Place, Weather
from weather.tools import GetWeather
CONTEXT = tool_context(
"weather",
google_maps_api_key=SecretStr("test-key"),
weather_api_url="https://api.open-meteo.com/v1/forecast",
)
class TestGetWeather(unittest.TestCase):
"""Test get_weather, with its services mocked."""
def test_weather(self):
now = Conditions(time="2026-10-15 14:00", temperature=18.5, precipitation=0, wind_speed=12, cloud_cover=40)
found = Weather(timezone="Europe/Paris", current=now, forecast=[now])
with patch("weather.tools.geocode", return_value=Place("Paris, France", 48.85, 2.35)), patch(
"weather.tools.weather", return_value=found
):
result = call_tool(GetWeather, {"location": "Paris"}, CONTEXT)
self.assertEqual(result["location"], "Paris, France")
self.assertEqual(result["units"]["wind_speed"], "km/h")
def test_unknown_place(self):
with patch("weather.services.googlemaps.Client") as client:
client.return_value.geocode.return_value = []
result = call_tool(GetWeather, {"location": "Atlantis"}, CONTEXT)
self.assertIn("Could not find Atlantis", result["error"])
def test_invalid_arguments(self):
self.assertIn("Invalid arguments", call_tool(GetWeather, {"unit": "KELVIN"})["error"])
def test_definition(self):
self.assertEqual(GetWeather.problems(), [])
self.assertEqual(GetWeather.parameters()["required"], ["location"])
test_definition checks the tool against the tool protocol.
problems() runs the same checks
that the platform runs when it loads the Extension, and returns a description of each problem: an
invalid name, a missing arguments_model, an unimplemented call(), or no description. An empty
list means the platform will load the tool. Checking
parameters(), the JSON Schema
that the LLM is offered, catches an arguments model that makes the wrong fields required.
Running the Tests
An Extension’s tests import Smarter, so they run in a Smarter development container, where
manage.py test_extension puts the repository on sys.path and runs its tests/:
docker cp ../smarter-extension-weather smarter-app:/tmp/
docker exec smarter-app bash -c "cd /home/smarter_user/smarter && python manage.py test_extension /tmp/smarter-extension-weather"
Before you publish, also check the packaged Extension as the installer will, with
manage.py validate_extension, and install it on your development platform to try it with an
LLM. See Building an Extension.