On this page

Testing and publishing

Module tests are ordinary pytest. The SDK deliberately ships no test double: the context is defined by Protocols, so a test fakes exactly what a handler touches.

Unit tests#

modules/my_module/tests/test_ranking.py
import asyncio
import pandas as pd

from modules.my_module.module import MyModule


class FakeEventLog:
    def __init__(self, rows):
        self._rows = rows

    async def __aenter__(self):
        return self

    async def __aexit__(self, *exc):
        return None

    async def duckdb_fetch(self, sql, params=None):
        return self._rows


class FakeCache:
    def __init__(self):
        self.values = {}

    async def get(self, key):
        return self.values.get(key)

    async def set(self, key, value):
        self.values[key] = value


class FakeContext:
    log_id, module_id, user_id = "log-1", "my_module", "user-1"

    def __init__(self, rows):
        self.event_log = FakeEventLog(rows)
        self.cache = FakeCache()


def test_ranking_is_sorted_descending():
    ctx = FakeContext([("a", 10), ("b", 4)])
    out = asyncio.run(MyModule().ranking(ctx))
    assert [row["activity"] for row in out["rows"]] == ["a", "b"]

Mirror the Protocol signatures from mate.sdk.context so a fake cannot drift from the real contract, and name tests as expectations.

LayerTest with
Pure computationPlain functions — fast, and where the subtle bugs live.
Handler wiringFakes for the two or three context members it uses.
Dataset payloadsShape assertions against the declared shape.
FrontendA manual pass: loading, empty, and error states; live updates; dark mode and compact density; the card at exactly min_px_w × min_px_h.
Terminal
uv run pytest modules/my_module/tests -v         # your module
make test                                        # the platform suite, incl. module fixtures
uv run pyright                                   # strict types
CI for a module
- uses: astral-sh/setup-uv@v5
- run: uv sync --extra dev
- run: uv run python -c "from mate.sdk import Manifest; Manifest.load_yaml('modules/my_module/manifest.yaml')"
- run: uv run pytest modules/my_module/tests

Validating the manifest first is deliberate: it is the cheapest check and the most common failure.

Distribution channels#

ChannelInputBest for
UploadA .zip, .tar, .tar.gz, or .tgz of the folderPrivate modules, one-off installs, air-gapped hosts.
Installed packageA package exposing the mate.modules entry pointModules shipped through your own infrastructure — discovery picks it up at boot; the UI has no registry browser.

Install runs as a job and rolls back cleanly on failure. Git-URL and registry channels are specified in docs/INSTRUCTIONS.md but not implemented yet. Package an archive with the manifest, module.py, tests, and frontend sources — exclude .venv/, .dist/, node_modules/, and large model files (those belong in model_store).

Versioning#

Bump version: whenever results change, not only when code changes. A results-affecting change should invalidate caches — version your cache keys or delete them on first run. Renaming a config_schema property orphans stored values, so add the new name and read both for one release.

Author checklist#

  • [ ] Manifest.load_yaml(...) passes, and id matches module.py.
  • [ ] log_model matches the log the module can actually handle.
  • [ ] Every emitted topic is in provides:; every subscription and capability call is in consumes: or optional_modules:.
  • [ ] Precompute ordering uses <module_id>.completed (or requirements.modules), not hope; the handler is idempotent and rebuilds from ctx.event_log.
  • [ ] Long jobs use @job(progress=True) and check cancellation at loop boundaries.
  • [ ] Results live in ctx.cache under keys that encode the parameters that change them.
  • [ ] No imports from apps/api/* or apps/web/*.
  • [ ] Widgets use the shared kit and declare min_px_w/min_px_h measured in a browser.
  • [ ] .venv/, .dist/, node_modules/ are gitignored; uv.lock is committed.
  • [ ] The README states what the module computes and any external dependency.

The symptoms and fixes for the failures that remain are in Troubleshooting.