Writing scenarios
A scenario is a Rhai script. The top level can be the whole test, or you can register several named scenarios as a suite.
Agents and call control
agent(name, #{ … }) connects a headless baresip instance
and returns a handle you drive with verbs — register,
dial, accept,
hangup, hold, dtmf, transfer, … See
Agents for the full set, the config options and the readable
state (registered, state, …).
await_until
SIP is asynchronous, so assertions are polled:
await_until re-runs an
assert(...) until it holds or a timeout
elapses. Use it instead of sleeping.
#![allow(unused)]
fn main() {
a.dial(b);
await_until(|| assert(b.state).equals(State::Ringing), "15s");
}
The matchers — equals,
is_true,
contains, … — are all on the assertion
handle.
Suites: setup / scenario / teardown
setup() runs before each scenario and returns
the context passed to it; each
scenario(name, body) runs in isolation with
fresh agents; teardown() runs after each
(even on failure).
#![allow(unused)]
fn main() {
setup(|| {
let caller = agent("Caller", #{
username: env("A_USER"),
domain: env("SIP_DOMAIN"),
password: env("A_PASS"),
});
caller.register();
await_until(|| assert(caller.registered).is_true(), "10s");
#{ caller: caller }
});
scenario("answered call", #{ tags: ["smoke"] }, |ctx| {
ctx.caller.dial("+49301234567");
await_until(|| assert(ctx.caller.state).equals(State::Established), "15s");
});
}
Selecting, tagging and skipping
The scenario(name, #{ … }, body) options
control which scenarios run:
- Tags —
#{ tags: ["smoke"] }, then--tag smoke/--exclude-tag slow. - Skip —
#{ skip: true | "reason" }disables a scenario statically; or callskip("reason")at runtime (e.g. env-gated). - Focus —
#{ only: true }runs only the focused scenario(s), run-wide.
Skipped scenarios are reported but don’t fail the run.
More
- Assertions and matchers — the full matcher set.
- Audio testing — send tones/files and assert what’s received.
- HTTP & webhooks — call and mock a backend API.