Testing & Development

Test harness, pytest fixtures, development setup, building, and release process

2
context managers
2
pytest fixtures
pytest
test framework
make
build system

Test Harness

The merobox.testing module provides context managers and pytest fixtures for spinning up clusters and running workflows in test suites.

cluster() Context Manager

Spins up an isolated cluster of merod nodes for the duration of the block, tearing down on exit.

from merobox.testing import cluster

async with cluster(
  count=3,                # number of nodes to spawn
  prefix="test",           # name prefix for containers
  image="merod:latest",     # Docker image to use
) as env:
  # env is a dict with:
  # nodes — list of node names
  # endpoints — dict mapping node name to URL
  assert len(env["nodes"]) == 3

pytest_cluster Fixture

A pytest fixture that wraps cluster() for seamless test integration.

import pytest
from merobox.testing import pytest_cluster

@pytest.fixture(scope="module")  # scope: function | class | module | session
async def my_cluster():
  async with pytest_cluster(scope="module") as env:
    yield env

async def test_node_health(my_cluster):
  for name, url in my_cluster["endpoints"].items():
    # assert node is reachable
    ...

Workflow Testing

workflow() Context Manager

Executes a workflow YAML file and exposes the resulting environment for assertions.

from merobox.testing import workflow

async with workflow(
  workflow_path="tests/e2e.yaml",
  prefix="e2e",
) as env:
  # env is a dict with:
  # workflow_result — final StepResult
  # nodes — list of node names
  # endpoints — dict mapping node name to URL
  # manager — NodeManager instance for direct control
  assert env["workflow_result"].success

pytest_workflow Fixture

A full-featured pytest fixture for workflow-based integration tests.

from merobox.testing import pytest_workflow

@pytest.fixture(scope="module")
async def e2e_env():
  async with pytest_workflow(
    workflow_path="tests/e2e.yaml",
    prefix="e2e",
    image="merod:latest",
    chain_id="localnet",
    wait_for_ready=True,
    scope="module",
  ) as env:
    yield env

async def test_workflow_passes(e2e_env):
  assert e2e_env["workflow_result"].success

Fixture Parameters

  • workflow_path — path to the workflow YAML file
  • prefix — name prefix for containers and resources
  • image — Docker image override for merod nodes
  • chain_id — NEAR chain ID for sandbox configuration
  • wait_for_ready — block until all nodes are healthy
  • scope — pytest fixture scope (function, class, module, session)

Environment Dict Keys

  • workflow_result — final StepResult from the workflow execution
  • nodes — list of node name strings
  • endpoints — dict mapping node names to HTTP endpoint URLs
  • manager — the active NodeManager instance for direct node control

Development Setup

Prerequisites

3.9–3.11
Python
20.10+
Docker
pip
package manager

Local Development Setup

1

Clone and enter the repository

git clone https://github.com/calimero-network/merobox.git
cd merobox
2

Create a virtual environment

python -m venv .venv
source .venv/bin/activate
3

Install dependencies

pip install -e ".[dev]"
# or: make install

Building

All build tasks are managed via make. Run make help for the full list.

make help
Display all available make targets with descriptions.
make build
Build the package distribution (wheel + sdist) into the dist/ directory.
make check
Run all linters: ruff, mypy, and format checking. Fails on any violation.
make install
Install merobox in editable mode with all dev dependencies.
make format
Auto-format all Python files with ruff format and isort.
make format-check
Check formatting without modifying files. Returns non-zero on violations.

Running Tests

Tests use pytest with async support via pytest-asyncio.

All tests

make test
# equivalent to: pytest tests/ -v

Specific tests

pytest tests/test_workflow.py -v
pytest tests/ -k "test_cluster" -v

Adding New Commands

merobox uses Click for CLI command definitions. Each command lives in its own file under merobox/commands/.

1

Create the command file

Create a new file in merobox/commands/, e.g. merobox/commands/my_command.py.

2

Implement the Click command

import click

@click.command()
@click.argument("name")
@click.option("--verbose", is_flag=True, help="Enable verbose output")
def my_command(name, verbose):
  """Short description of the command."""
  ...
3

Register the command

Add the import to merobox/commands/__init__.py and register with the CLI group.

from .my_command import my_command

# In the CLI group registration:
cli.add_command(my_command)
4

Update __all__

Add the command name to __all__ in merobox/commands/__init__.py for clean imports.

Adding New Step Types

Step types extend BaseStep and register with the step factory for use in workflows.

1

Create the step file

Create a new file in merobox/steps/, e.g. merobox/steps/my_step.py.

2

Inherit from BaseStep

from merobox.steps.base import BaseStep, StepResult, ok, fail

class MyStep(BaseStep):
  step_type = "my_step"
3

Implement required methods

def _get_required_fields(self) -> list[str]:
  return ["target", "value"]

def _validate_field_types(self) -> None:
  # Validate config field types
  if not isinstance(self.config["target"], str):
    raise StepValidationError("target must be a string")

async def execute(self, context) -> StepResult:
  # Step logic here
  return ok({"result": "value"})
4

Register in the step factory

Add the step type mapping in the executor’s step factory registry.

# In merobox/executor.py or step factory
STEP_REGISTRY["my_step"] = MyStep

Release Process

Releases are version-gated and fully automated once a tag is pushed.

Version Source of Truth

The version string lives in merobox/__init__.py and is the single source of truth for the package version.

# merobox/__init__.py
__version__ = "0.4.2"

Automated Pipeline

1

Auto-tag

CI detects version bump in __init__.py and creates a git tag v0.4.2.

2

Build binaries

Wheel and sdist are built for all supported platforms (Linux, macOS, Windows).

3

GitHub Release

A GitHub Release is created with the tag, changelog, and build artifacts attached.

4

PyPI Publish

The wheel and sdist are uploaded to PyPI for pip install merobox access.

Project Structure

merobox/
  __init__.py          # Version (__version__), package root
  cli.py               # Click CLI entry point
  executor.py          # WorkflowExecutor orchestration
  config.py            # Configuration loading and merging
  errors.py            # MeroboxError hierarchy
  commands/             # CLI command implementations
    __init__.py
    run.py             # merobox run
    stop.py            # merobox stop
    health.py          # merobox health
    logs.py            # merobox logs
    nuke.py            # merobox nuke
    remote.py          # merobox remote
    bootstrap.py       # merobox bootstrap
  steps/               # Workflow step implementations
    base.py            # BaseStep, StepResult, ok(), fail()
    call.py            # call step
    assert_step.py     # assert step
    parallel.py        # parallel step
    repeat.py          # repeat step
    script.py          # script step
    ...
  managers/             # Node management backends
    docker.py          # DockerManager
    binary.py          # BinaryManager
    sandbox.py         # SandboxManager
    remote.py          # RemoteNodeManager
  testing/              # Test harness module
    __init__.py        # cluster(), workflow(), fixtures
  auth/                # Authentication
    manager.py         # AuthManager, JWT handling
tests/                 # Test suite
Makefile               # Build and task automation
pyproject.toml         # Package configuration