Skip to content

Development and tests

To work on Hailer itself:

git clone https://github.com/OpenAfterHours/hailer.git
cd hailer
uv sync --locked
uv run hailer login openai
uv run hailer

Use uv run hailer ... in the checkout so you run the code you are changing. The repository already includes hailer.toml, a starter notebook and the data folder. Configure another provider in hailer.toml before logging in if needed.

The checkout also has optional sample data: six months of synthetic sales orders, with an extra column in later months. Generate it before starting Hailer:

uv run python scripts/make_sample_data.py          # writes data/25-01 sales.parquet ... 25-06
uv run python scripts/make_sample_data.py --help   # --out, --months, --rows, --start, --seed, --dataset

Hailer pins marimo==0.24.2 because it drives the private marimo._code_mode API, which has no stability guarantee. The agent uses langchain, langchain-openai and langgraph-checkpoint-sqlite within their declared major-version bounds. Upgrade deliberately and re-run the tests.

Tests

uv run pytest

The suite is offline: it needs no API key, no marimo server, no model endpoint, no network and no Docker. The marimo protocol is exercised against a local fake server (tests/fake_marimo.py, token-checked like a server Hailer starts), the agent against a scripted chat model and against a strict Chat-Completions-only fake gateway on loopback (tests/fake_gateway.py, which rejects unknown request fields and model names the way internal gateways do), and the credential store against an in-memory backend. The kernel runtimes are tested without starting anything: tests/test_kernel.py (the kernel's environment, the unsafe-local runtime with its processes faked), tests/test_sandbox.py (notebook files through marimo's file API, served by tests/fake_marimo.py; notebook names that could leave the folder are refused before any request), tests/test_kernel_docker.py (the docker runtime against tests/fake_docker.py, a scripted docker CLI that keeps containers and networks with ids, labels and --filter, like Docker 29), tests/test_kernel_image.py, tests/test_forward.py (the forwarder on real loopback sockets) and tests/test_build_kernel_image.py. tests/fake_kernel.py holds the shared pieces.

tests/test_docker_integration.py drives the real docker runtime: it starts a kernel for a temporary workspace with sample sales data, opens the notebook in headless Chrome, and checks that code runs as a non-root user in /work, a code-mode cell lands in the host notebook, writes to the data folder and the network are refused, no host secret or the server token is visible, and stopping leaves nothing behind. It is opt-in and never pulls the image:

uv run python -m scripts.build_kernel_image --load     # the image for this checkout, into local Docker
HAILER_DOCKER_TESTS=1 uv run pytest tests/test_docker_integration.py -rs

(In PowerShell, set the variable first: $env:HAILER_DOCKER_TESTS = "1".) HAILER_DOCKER_TESTS=1 skips, with the reason, when Docker, the image or a browser is missing; HAILER_DOCKER_TESTS=strict fails instead. HAILER_TEST_CHROME picks the browser (otherwise google-chrome or chromium on PATH, or Chrome's usual install folder on Windows and macOS), and HAILER_KERNEL_IMAGE another image.

.github/workflows/test.yml runs the suite on Ubuntu and Windows with Python 3.12 and 3.13 for every push to main and every pull request; the repository requires those four checks by name. A fifth job, Docker kernel, builds the image from the checkout on Ubuntu and runs the integration test with HAILER_DOCKER_TESTS=strict (Linux only: GitHub's Windows runners only run Windows containers). It is not a required check, but the release waits for it.