Sigil Testing
First-Class Tests
Sigil tests are first-class language declarations, not a separate test framework. For the shared debugging workflow across inspect, run, test, replay, and stepping, see [DEBUGGING.md](/sigil/docs/debugging/).
Repo-level integration tests are ordinary Sigil test files under language/integrationTests/tests/. They run through the same sigil test machinery as project tests rather than through separate shell launchers.
Canonical Layout
sigil.jsonis the mode switch- in project mode, tests live under
tests/ - in standalone mode,
testdeclarations may live directly in ordinary.sigilfiles - test files are ordinary
.sigilfiles - test files may include helpers alongside
testdeclarations
Project application/library code should live under src/ and be referenced from tests with rooted module syntax. Standalone files use local names instead.
Referencing Real Modules
Library code is file-based, not export-based:
λcompletedCount(todos:[µTodo])=>Int=todos reduce (λ(acc:Int,todo:µTodo)=>Int match todo.done{
true=>acc+1|
false=>acc
}) from 0
λmain()=>Unit=()
test "count completed todos" {
•todoDomain.completedCount([
{
done:true,
id:1,
text:"A"
},
{
done:false,
id:2,
text:"B"
}
])=1
}
Test Syntax
λmain()=>Unit=()
test "adds numbers" {
1+1=2
}
Rules:
- test description is a string literal and may span lines
- test body must evaluate to
Bool truepassesfalsefails
In project code, named project types still live in src/types.lib.sigil and tests refer to them through µ....
Effectful tests use explicit effects:
λmain()=>Unit=()
test "writes log" =>!Log {
l _=(§io.println("x"):Unit);
true
}
Tests may also derive a local world:
λmain()=>Unit=()
test "worlds capture logs" =>!Log world {
c log=(†log.capture():†log.LogEntry)
} {
l _=(§io.println("captured"):Unit);
※check::log.contains("captured")
}
Multiline descriptions use the same string syntax:
λmain()=>Unit=()
test "multiline
test description works" {
§string.lines("alpha
beta")=[
"alpha",
"beta"
]
}
Worlds, Observation, and Coverage
Sigil no longer treats tests as code plus ad hoc mocks.
Instead:
config/exports the baseline.lib.sigil world- that same config module may also export selected env declarations such as
flags, available to app code as •config.flags
- each
testmay derive that world locally withworld { ... } - standalone files may instead provide a local top-level
c world=(...:†runtime.World)with no--env †...builds world entries forClock,Fs,FsWatch,Http,Log,Process,Pty,Random,Stream,Tcp,Timer, andWebSocket※observe::...exposes raw traces from the active test world※check::...exposes Bool-returning helpers over those traces
Canonical split:
†is compiler-owned runtime world construction※observeis raw test-world inspection※checkis ergonomic Bool helpers for tests
Canonical note:
- if a world entry exists only as shared baseline behavior, keep it in
config/ instead of restating it in every test
Example:
λmain()=>Unit=()
test "captured log contains line" =>!Log world {
c log=(†log.capture():†log.LogEntry)
} {
l _=(§io.println("captured"):Unit);
※check::log.contains("captured")
}
Canonical named-boundary helpers include:
※observe::file.readTextAt※observe::fsWatch.eventsAt※observe::fsWatch.watchesAt※observe::log.entriesAt※observe::pty.spawnsAt※observe::pty.writesAt※observe::process.commandsAt※observe::websocket.receivedAt※observe::websocket.sentAt※check::file.existsAt※check::file.textEqualsAt※check::fsWatch.closedAt※check::fsWatch.watchingAt※check::log.containsAt※check::pty.closedAt※check::pty.spawnedOnceAt※check::process.calledOnceAt※check::websocket.connectedOnceAt※check::websocket.sentAt
For topology-aware labelled-boundary projects, these helpers are the canonical testing surface. They let a test assert the observed effect at the exact named boundary instead of inferring it from ambient global state.
Example:
λmain()=>Unit=()
test "audit sink receives redacted ssn" =>!Fs!Log!Process world {
c exports=(†fs.sandboxRoot(
".local/labelled-boundaries-tests/audit",
•topology.exportsDir
):†fs.FsRootEntry)
} {
l _=(•app.runExample():Unit);
※check::log.containsAt(
"***-**-6789",
•topology.auditLog
)
}
sigil test also enforces project-surface coverage for project source modules:
- every project
src/*.lib.sigilfunction must be executed by the suite - sum-returning project functions must observe each relevant output variant
- missing surface coverage is reported as ordinary failing test results
- this coverage gate applies to suite-style runs such as
sigil testorsigil test path/to/tests/ - focused single-file runs such as
sigil test path/to/tests/file.sigilskip the project-wide coverage gate - non-project directory runs such as
sigil test language/examplesrecurse over.sigilfiles and run embedded tests without any project coverage gate
Library tests may call •... exports directly. Executable tests exercise program behavior through main.
CLI
Default output mode is JSON. This section keeps the test-specific surface. For the broader debugging workflow and when to choose inspect, run, test, or debug, use [DEBUGGING.md](/sigil/docs/debugging/).
Examples:
# Run all tests in the current project tests/ directory
cargo run -q -p sigil-cli --no-default-features -- test
# Run the self-testing language examples
cargo run -q -p sigil-cli --no-default-features -- test language/examples
# Run a specific file or subdirectory
cargo run -q -p sigil-cli --no-default-features -- test projects/algorithms/tests/basicTesting.sigil
# Filter by test name substring
cargo run -q -p sigil-cli --no-default-features -- test --match "cache"
# Trace one test file
cargo run -q -p sigil-cli --no-default-features -- test --trace projects/algorithms/tests/basicTesting.sigil
# Stop the current test when a function is reached
cargo run -q -p sigil-cli --no-default-features -- test --break-fn helper projects/algorithms/tests/basicTesting.sigil
# Record and replay a test run
cargo run -q -p sigil-cli --no-default-features -- test --record .local/tests.replay.json projects/algorithms/tests/basicTesting.sigil
cargo run -q -p sigil-cli --no-default-features -- test --replay .local/tests.replay.json projects/algorithms/tests/basicTesting.sigil
# Start a replay-backed stepping session for one exact test id
cargo run -q -p sigil-cli --no-default-features -- debug test start --replay .local/tests.replay.json --test "projects/algorithms/tests/basicTesting.sigil::cache hit returns cached value" --watch result.value projects/algorithms/tests/basicTesting.sigil
cargo run -q -p sigil-cli --no-default-features -- debug test step-into .local/debug/.json
For runtime-world projects, and for projects that read selected config declarations such as •config.flags, --env is required. sigil test --replay cannot be combined with --env; the replay artifact owns the resolved per-test world.
For process-heavy harness code, prefer:
§processfor child processes§file.makeTempDirfor scratch workspaces§time.sleepMsfor retry loops
JSON Output
test emits a single JSON object to stdout by default.
Top-level fields:
formatVersioncommandoksummaryresults
Each result currently includes:
idfilenamestatus
- pass | fail | error | stopped
durationMslocation- optional
failure - optional
trace - optional
breakpoints - optional
replay - optional
exception
summary now also includes:
stopped
Stop-mode breakpoint hits are not runtime errors:
- the current test result becomes
status: "stopped" - the suite keeps running later selected tests
- top-level
okis stillfalse
Replay-backed debug snapshots may also include:
watches
- ordered results for any configured --watch local(.field)* selectors
Current aggregated test output does not include:
declaredEffects- structured
assertionmetadata - raw per-test coverage traces
Formal references:
language/docs/DEBUGGING.mdlanguage/docs/TESTING_JSON_SCHEMA.mdlanguage/spec/cli-json.mdlanguage/spec/cli-json.schema.json