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#
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.
| Layer | Test with |
|---|---|
| Pure computation | Plain functions — fast, and where the subtle bugs live. |
| Handler wiring | Fakes for the two or three context members it uses. |
| Dataset payloads | Shape assertions against the declared shape. |
| Frontend | A manual pass: loading, empty, and error states; live updates; dark mode and compact density; the card at exactly min_px_w × min_px_h. |
uv run pytest modules/my_module/tests -v # your module
make test # the platform suite, incl. module fixtures
uv run pyright # strict types
- 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#
| Channel | Input | Best for |
|---|---|---|
| Upload | A .zip, .tar, .tar.gz, or .tgz of the folder | Private modules, one-off installs, air-gapped hosts. |
| Installed package | A package exposing the mate.modules entry point | Modules 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, andidmatchesmodule.py. - [ ]
log_modelmatches the log the module can actually handle. - [ ] Every emitted topic is in
provides:; every subscription and capability call is inconsumes:oroptional_modules:. - [ ] Precompute ordering uses
<module_id>.completed(orrequirements.modules), not hope; the handler is idempotent and rebuilds fromctx.event_log. - [ ] Long jobs use
@job(progress=True)and check cancellation at loop boundaries. - [ ] Results live in
ctx.cacheunder keys that encode the parameters that change them. - [ ] No imports from
apps/api/*orapps/web/*. - [ ] Widgets use the shared kit and declare
min_px_w/min_px_hmeasured in a browser. - [ ]
.venv/,.dist/,node_modules/are gitignored;uv.lockis committed. - [ ] The README states what the module computes and any external dependency.
The symptoms and fixes for the failures that remain are in Troubleshooting.