ringo-flow 0.10.1

Declarative telephony scenario test runner for baresip, built on ringo-core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# ringo-flow scenario API

Functions, getters and types available in a `.rhai` scenario. **Generated** from the engine with `ringo-flow docs` — do not edit by hand; see the [README](../README.md) for concepts and usage.

## Top-level

### `await_until(body: Fn) -> ?`

Re-run the expression until its assertion holds or the default timeout
elapses: `await_until(|| assert(a.registered).is_true())`. Returns the
body's value, so `.value()` can bind a verified value.

### `await_until(body: Fn, within: string) -> ?`

Like `await_until(body)` but with an explicit timeout, e.g. `"15s"`.

### `default_timeout(duration: string)`

Set the default `await_until` timeout for the rest of the script (e.g. `"10s"`).

### `env(name: string) -> string`

Read a variable: first from `--env-file`/`<scenario>.env`/`load_env`, then
the process environment. Errors if unset. Use it for per-env credentials.

### `load_env(path: string)`

Load a dotenv file (`KEY=VALUE` lines) into `env(...)` for this scenario,
resolved relative to the scenario file. Later loads override earlier keys.

### `log(message: string)`

Print a timestamped note to the scenario log (and the `--json` stream),
unlike `print` which writes a bare line.

### `parallel(tasks: array) -> array`

Run the given zero-arg closures concurrently and wait for all; returns
their results as an array, and fails if any task fails. Use it for
independent blocking work, e.g. `verify_audio` on several agents at once.
Tasks may share captured variables (each gets an independent snapshot,
so they can't race). Don't overlap `await_until` across tasks; its
silencing is global.

### `regex(pattern: string) -> PathPattern`

A regex path matcher for `respond`/`on`/`request_count`/… anchored to the
whole path: `regex("/calls/.*")` matches `/calls/123`. Errors on a bad
pattern.

### `scenario(name: string, body: Fn)`

Register a named scenario, run in isolation (fresh agents, torn down
after). The body may take the `setup()` context: `|ctx| { … }`.

### `scenario(name: string, options: map, body: Fn)`

Register a scenario with options `#{ tags: ["smoke"], skip: true|"reason",
only: true }`. `--tag`/`--exclude-tag` filter by tag; a skipped scenario is
reported but not run; if any scenario sets `only`, only those run.

### `setup(body: Fn)`

Run before each scenario; its return value is passed to the scenario
(and teardown) as `ctx`. Typically creates and registers the agents.

### `skip()`

Skip the current scenario at runtime (reported, not failed).

### `skip(reason: string)`

Skip the current scenario at runtime with a reason (reported, not failed);
e.g. `if env("STAGE") != "prod" { skip("prod only") }`.

### `teardown(body: Fn)`

Run after each scenario (even on failure); receives the `setup` context.

### `to_string(state: CallState) -> string`

The call state as a string.

### `uuid() -> string`

A fresh random UUID string.

### `wait(seconds: int)`

Hold for N seconds; FAILS if a call that is established at the start drops.

## Agents

### `abort_transfer(agent: Agent)`

Abort the pending attended transfer.

### `accept(agent: Agent)`

Answer the agent's incoming call.

### `agent(name: string, config: map) -> Agent`

Connect a headless baresip agent and return a handle.
`config` is a map: `username`/`domain` (required), `password`, `display_name`,
`transport`, `auth_user`, `outbound`, `stun_server`, `media_enc`, `regint`,
`mwi`, `dtmf_mode` (`"info"` for reliable headless DTMF), `headers`.

### `attended_transfer(agent: Agent, target: Agent)`

Start an attended transfer: place a consultation call to another agent.
Complete it with `complete_transfer()` once that call is established.

### `attended_transfer(agent: Agent, target: string)`

Start an attended transfer to a literal URI or bare number.

### `complete_transfer(agent: Agent)`

Complete the pending attended transfer (REFER with Replaces).

### `dial(agent: Agent, target: Agent)`

Dial another agent at its AOR.

### `dial(agent: Agent, target: string)`

Dial a literal SIP URI, or a bare number/extension in the agent's own domain.

### `dtmf(agent: Agent, digits: string)`

Send DTMF tones (characters `0-9`, `*`, `#`, `A-D`) back-to-back.

### `dtmf(agent: Agent, digits: string, gap: string)`

Send DTMF tones with a pause between digits, e.g. `dtmf("123#", "200ms")`.

### `get name(peer: Peer) -> ?`

The remote party's display name, or `()` if absent.

### `get number(peer: Peer) -> ?`

The remote party's number (user-part of the URI), or `()`.

### `get peer(agent: Agent) -> Peer`

The current call's remote party (the caller for an incoming call); read
`peer.uri` / `peer.number` / `peer.name` (each `()` if there's no call).

### `get reason(agent: Agent) -> ?`

The last closed call's reason (string), or `()` if none yet.

### `get registered(agent: Agent) -> bool`

Whether the agent's account is currently registered.

### `get state(agent: Agent) -> CallState`

The agent's current call phase: `Idle`, `Ringing` or `Established`.

### `get status_code(agent: Agent) -> ?`

SIP status code from the last closed call's reason (int, e.g. `603`),
or `()` if the reason isn't a SIP response (local hangup, reset, …).

### `get uri(peer: Peer) -> ?`

The remote party's full URI (e.g. `sip:bob@example.com`), or `()`.

### `hangup(agent: Agent)`

Hang up the agent's active call.

### `header(agent: Agent, name: string) -> ?`

Value of a header on a received INVITE (string), or `()` if absent.

### `headers(agent: Agent) -> map`

All received INVITE headers as a map (name → value); duplicates collapse,
use `header(name)` for a specific one.

### `hold(agent: Agent)`

Put the active call on hold.

### `info(agent: Agent) -> map`

A map of the agent's current state: name, aor, registered, state,
reason, status_code, calls. Handy to `print(...)` or assert on.

### `mute(agent: Agent)`

Toggle mute on the active call.

### `register(agent: Agent)`

(Re-)register the agent's account.

### `resume(agent: Agent)`

Resume a held call.

### `to_json(agent: Agent) -> string`

The agent's current state as a JSON string (for `log(...)`/debugging).

### `transfer(agent: Agent, target: Agent)`

Blind-transfer (REFER) the active call to another agent's AOR.

### `transfer(agent: Agent, target: string)`

Blind-transfer (REFER) the active call to a literal URI or bare number.

## Assertions & matchers

### `assert(actual) -> Assertion`

Begin a fluent assertion on a value: `assert(x).equals(y)`, `.is_true()`,
`.greater_than(n)`, etc. Matchers chain (`.at_least(200).at_most(299)`)
and error (with a value-based message) on a mismatch. Asserting on a
getter auto-labels the log line (`assert(caller.state)` → `Caller state`,
`assert(res.status)` → `HTTP status`); `.describe(…)` overrides.

### `at_least(a: Assertion, n: int) -> Assertion`

Assert the (numeric) value is >= `n`.

### `at_most(a: Assertion, n: int) -> Assertion`

Assert the (numeric) value is <= `n`.

### `contains(a: Assertion, needle: string) -> Assertion`

Assert the (string) value contains `needle`.

### `describe(a: Assertion, label: string) -> Assertion`

Label this assertion so the log line names it: `assert(caller.registered)
.describe("caller registered").is_true()` → `caller registered: ✓ expect …`.

### `equals(a: Assertion, expected) -> Assertion`

Assert the value equals `expected` (`is` is a reserved word in Rhai).

### `greater_than(a: Assertion, n: int) -> Assertion`

Assert the (numeric) value is > `n`.

### `is_absent(a: Assertion) -> Assertion`

Assert the value is absent (`()`).

### `is_empty(a: Assertion) -> Assertion`

Assert the string/array/map value is empty.

### `is_false(a: Assertion) -> Assertion`

Assert the value is `false`.

### `is_not_empty(a: Assertion) -> Assertion`

Assert the string/array/map value is not empty.

### `is_present(a: Assertion) -> Assertion`

Assert the value is present (not `()`), e.g. a received header.

### `is_true(a: Assertion) -> Assertion`

Assert the value is `true`.

### `less_than(a: Assertion, n: int) -> Assertion`

Assert the (numeric) value is < `n`.

### `matches(a: Assertion, pattern: string) -> Assertion`

Assert the (string) value matches the regex `pattern`.

### `not_equals(a: Assertion, expected) -> Assertion`

Assert the value does not equal `expected`.

### `value(a: Assertion) -> ?`

The value under assertion, so a verified value can be bound:
`let id = await_until(|| assert(callee.header("X-Id")).is_present().value());`.

## HTTP

### `expect_status(response: HttpResponse, code: int)`

Assert and report the status; errors on mismatch.

### `get body(response: HttpResponse) -> string`

The HTTP response body as a string.

### `get status(response: HttpResponse) -> int`

The HTTP response status code.

### `header(response: HttpResponse, name: string) -> ?`

A response header value (string), or `()` if absent.

### `http(method: string, url: string) -> HttpResponse`

Make an HTTP request and return the response.

### `http(method: string, url: string, options: map) -> HttpResponse`

Make an HTTP request with options `#{ headers: #{…}, body: … }`.
`body` may be a string or a map (encoded to JSON).

### `json(response: HttpResponse) -> ?`

The whole JSON body as a native value (object→map, array, …).

### `json(response: HttpResponse, path: string) -> ?`

The value at a dotted JSON path (e.g. `"data.id"`), typed: object→map,
array, number, bool, `null`→`()`. Errors if the path is missing.

## HTTP mock server

### `get body(request: MockRequest) -> string`

The raw request body.

### `get method(request: MockRequest) -> string`

The request method (upper-case).

### `get path(request: MockRequest) -> string`

The request path.

### `get port(mock: HttpMock) -> int`

The port the server is listening on.

### `get url(mock: HttpMock) -> string`

The server's base URL, e.g. `http://127.0.0.1:8080`.

### `header(request: MockRequest, name: string) -> ?`

A request header value (case-insensitive), or `()` if absent.

### `json(request: MockRequest, path: string) -> ?`

The value at a dotted JSON path in the body (object→map, array, number,
bool, `null`→`()`). Errors if the path is missing.

### `json_response(body) -> map`

Build a `200 application/json` response map from `body` (JSON-encoded),
for `respond`/`on`. `body` may be a map or an array, e.g.
`json_response(#{ actions: [ … ] })` or `json_response([ … ])`.

### `last_request(mock: HttpMock, path: PathPattern) -> MockRequest`

The most recent request on a `regex(...)` path (errors if none yet).

### `last_request(mock: HttpMock, path: string) -> MockRequest`

The most recent request on `path` (errors if none yet). Read it after
`await_until` confirms the webhook arrived.

### `mock_server() -> HttpMock`

Start a mock HTTP server on a free port and return a handle. Stopped
automatically at the end of the scenario. Use `url` to point the system
under test at it, `respond`/`on` to define routes.

### `mock_server(config: map) -> HttpMock`

Start a mock HTTP server with config `#{ port: 8080 }` (omit `port` for a
free one). Returns a handle; stopped automatically at scenario end.

### `on(mock: HttpMock, method: string, path: PathPattern, responder: Fn)`

Dynamic responder for `method` and a `regex(...)` path.

### `on(mock: HttpMock, method: string, path: string, responder: Fn)`

Answer `method path` dynamically: the `|req|` closure receives the
`MockRequest` and returns a response map (e.g. `json_response(#{…})`).
`method` may be `"*"` for any method. The closure runs on a runtime
worker, so keep it pure (request → response): no agent verbs, no `wait`
— those block a worker thread.

### `on(mock: HttpMock, path: PathPattern, responder: Fn)`

Dynamic responder for a `regex(...)` path on any HTTP method.

### `on(mock: HttpMock, path: string, responder: Fn)`

Dynamic responder for `path` on any HTTP method.

### `query(request: MockRequest, name: string) -> ?`

A query-string parameter value, or `()` if absent.

### `request_count(mock: HttpMock, path: PathPattern) -> int`

How many requests arrived on a `regex(...)` path (any method).

### `request_count(mock: HttpMock, path: string) -> int`

How many requests arrived on `path` (any method). Poll it with
`await_until`, e.g.
`await_until(|| assert(hooks.request_count("/voice")).equals(1))`.

### `requests(mock: HttpMock, path: PathPattern) -> array`

All requests on a `regex(...)` path, in arrival order, as `MockRequest`s.

### `requests(mock: HttpMock, path: string) -> array`

All requests received on `path`, in arrival order, as `MockRequest`s.

### `respond(mock: HttpMock, method: string, path: PathPattern, response: map)`

Static response for `method` and a `regex(...)` path.

### `respond(mock: HttpMock, method: string, path: string, response: map)`

Register a static response for `method path`: a map
`#{ status: 200, content_type: "…", headers: #{…}, body: <string|map> }`
(use `json_response`/`text_response` to build it). `method` may be `"*"`
for any method. Re-register to stage the next answer between webhooks.

### `respond(mock: HttpMock, path: PathPattern, response: map)`

Static response for a `regex(...)` path on any HTTP method.

### `respond(mock: HttpMock, path: string, response: map)`

Static response for `path` on any HTTP method.

### `stop(mock: HttpMock)`

Stop the server now (it otherwise stops automatically at scenario end).

### `text_response(body: string) -> map`

Build a `200 text/plain` response map from `body`, for `respond`/`on`.

## Audio

### `file(path: string) -> AudioSpec`

A WAV-file audio source, for `send_audio`.

### `send_audio(agent: Agent, source: AudioSpec)`

Switch the agent's active-call audio source: `tone(Hz)`, `file(path)` or `silent()`.

### `silent() -> AudioSpec`

A silent audio source (stop sending), for `send_audio`.

### `tone(freq: int) -> AudioSpec`

A sine-tone audio source at the given frequency (Hz), for `send_audio`.

### `verify_audio(agent: Agent, freq: int, within: string)`

Assert the agent is receiving a tone at `freq` Hz within the window (Goertzel).

### `verify_audio_connection(a: Agent, b: Agent)`

Assert two-way audio between two agents (a→b then b→a) at 1000 Hz.