Test framework#
Harn includes a built-in test runner invoked via harn test.
Running tests#
harn test path/to/tests/ # run all test files in a directory
harn test path/to/test_file.harn # run tests in a single file
Tests are ordinary unprivileged modules by default. A Rust-hosted service may
use harn test --trusted-host-dispatch path/to/test_file.harn to compile the
test and its private import graph with the same explicit privileged-wire
authority as its host-selected production route graph. The flag does not
change the authority of modules loaded by ordinary Harn imports.
Test discovery#
The test runner scans .harn files for pipelines whose names start with
test_. Each such pipeline is executed independently. A test passes if
it completes without error; it fails if it throws or an assertion fails.
pipeline test_addition(harness: Harness) {
assert_eq(1 + 1, 2)
}
pipeline test_string_concat(harness: Harness) {
const result = "hello" + " " + "world"
assert_eq(result, "hello world")
}
Pipelines can also opt in with @test, including table-driven cases. A
@test(cases: [...]) attribute creates one independent test case per row.
Each row must be a dict with a unique string name and an args list whose
length matches the pipeline parameter count. Reports and filters use the
display name pipeline[row].
@test(cases: [
{name: "positive", args: [2, 3, 5]},
{name: "negative", args: [-2, 1, -1]},
])
pipeline add_case(harness: Harness, left, right, expected) {
assert_eq(left + right, expected)
}
Reusable fixtures#
@test_fixture(scope: file|case) marks a zero-argument function with an
explicit return type as reusable test setup. Select it by name with
@test(fixture: fixture_name); the fixture value is injected as the test
pipeline's first parameter and table-row args supply the remaining
parameters.
@test_fixture(scope: file)
fn fixture() -> dict {
return {prefix: "user", rows: []}
}
@test(
cases: [
{name: "alice", args: ["alice", "user:alice"]},
{name: "bob", args: ["bob", "user:bob"]},
],
fixture: fixture,
)
pipeline test_query(
harness: Harness, fx: dict, input: string, expected: string,
) {
fx.rows.push(input)
assert_eq("${fx.prefix}:${input}", expected)
}
A file fixture runs once for the selected cases that reference it. Its
return value must be isolate-safe data: scalars, bytes, ranges, and nested
lists, dicts, sets, structs, enums, or pairs. Each case receives an isolated
copy-on-write clone, so mutation in one case cannot leak to a sibling.
Execution-bound values such as closures, channels, atomics, task/resource
handles, generators, streams, iterators, or harness capabilities are rejected
as one file-level setup failure.
A case fixture runs inside each case's fresh VM immediately before the test
pipeline. Use it for resources or other execution-bound values. The fixture
and test share one timeout and one pipeline lifecycle. VM/resource drop is the
teardown contract for both scopes; there is no separate teardown hook.
If file fixture setup fails, the runner emits one named file-level failure,
suppresses only the cases that reference that fixture, and continues other
files and unrelated tests. --fail-fast stops before case scheduling instead.
Fixture declarations, references, scopes, arity, row shape, and row names are
validated during discovery with source locations.
Assertions#
Three assertion builtins are available. They can be called anywhere, but they are intended for test pipelines and the linter warns on non-test use:
| Function | Description |
|---|---|
assert(condition, message?) | Throws if condition is falsy |
assert_eq(a, b, message?) | Throws if a != b, showing both values |
assert_ne(a, b, message?) | Throws if a == b, showing both values |
All three accept an optional custom message. If message is omitted, nil,
an empty string, or the literal string "null" (the common result of
json_stringify-ing a value that turned out to be nil), the assertion falls
back to its default message instead of throwing that uninformative value
verbatim.
Captured output#
log, print, println, and related output builtins write into a
per-case buffer rather than directly to the terminal. A passing test stays
quiet by default; a failing test always prints its buffered output
alongside the failure. Pass --verbose to see a passing test's captured
output too. --json-out and --junit reports include it under
captured_output (JUnit: <system-out>) whenever it is non-empty.
Mock LLM provider#
During harn test, the HARN_LLM_PROVIDER environment variable is
automatically set to "mock" unless explicitly overridden. The mock
provider returns deterministic placeholder responses, allowing tests
that call harness.llm.call, harness.llm.stream, or
harness.llm.stream_call to run without API keys.
CLI options#
| Flag | Description |
|---|---|
--filter <pattern> | Only run tests whose names contain <pattern> |
--verbose / -v | Show per-test timing and detailed failures |
--timing | Show per-test timing and summary statistics |
--timeout <ms> | Per-test timeout in milliseconds (default 30000) |
--parallel | Run user tests in a bounded pool, or conformance tests in isolated worker processes |
--fail-fast | Stop scheduling new tests after the first failure; already-running parallel tests finish |
--shard-index <N> / --shard-total <N> | Select one deterministic user-test or conformance shard |
--timing-environment <name> | Stamp the enforcing environment into a user-test JSON receipt |
--timing-baseline <path> | Balance user-test shards from a matching Harn receipt and enforce absolute case-cost regressions |
--junit <path> | Write JUnit XML report to <path> |
--record | Record LLM responses to .harn-fixtures/ |
--replay | Replay LLM responses from .harn-fixtures/ |