Scenarios & Configuration
A scenario is a sequence of HTTP requests run in order, defined across one or more .sinq files.
Writing Requests
A .sinq file is a standard HTTP request. You can define multiple requests in a single file by separating them with the ### delimiter. The text after ### on the same line names the request that follows it. This name shows up in --list output and in the standard/JUnit reports.
A leading ### (before any request content) works the same way and names the file's first request, even if the file only contains one:
To make your requests dynamic, you can use Lua scripts. sinq provides two ways to run scripts:
- Inline Interpolation (
${...}): Used to dynamically insert values into headers, URLs, or JSON payloads (e.g.,${env.BASE_URL}). - Lifecycle Hooks (
$PRE,$ASSERT, etc.): Used to control the flow of the request, such as failing a test if a status code is wrong, or polling until a background job completes.
Configuration
You can configure timeouts, environment variables, and limits using JSON files ending in .scenario. The filename itself is arbitrary, only the extension matters (e.g. config.scenario, 00_base.scenario).
When you place a .scenario file in a directory, those settings apply to all tests in that directory and its subdirectories.
Available settings:
{
"name": "My API Test",
"description": "Tests the core user flow",
"env": {
"BASE_URL": "https://api.local"
},
"req_timeout": "5s",
"script_timeout": "5s",
"timeout": "10m",
"fail_fast": true,
"max_retries": 10,
"max_redirects": 5,
"max_body": "1MiB",
"env_matrix": [],
"tags": []
}
name: Scenario name, shown in reports and used by the-n/--no-namefilters. Defaults to the leaf directory path. If the same.scenariofile is inherited by several leaf directories, they all take this name.description: Free-text description of the scenario.env: Variables defined here become accessible in your.sinqfiles as${env.VARIABLE_NAME}or in Lua scripts via theenvtable.req_timeout: Maximum time to wait for a single HTTP network request.script_timeout: Maximum time a single Lua script block is allowed to run.timeout: Maximum time allowed for the entire scenario to complete.fail_fast: If true, the scenario aborts immediately upon the first assertion failure.max_retries: The maximum amount of times any request in the scenario can be retried upon retry script returning a valid non-negative number.max_redirects: The maximum amount of redirects the client will follow before returning the redirect as the actual response.max_body: The maximum response body size stored in memory. Responses exceeding this are safely truncated.env_matrix: Data sets for running the scenario once per combination (parameterized / matrix testing). Aggregated along the path. See Environment Matrix.tags: Labels attached to every scenario that inherits this file. Collected into one list along the path and matched by the-t/--tagand--no-tagfilters.
The Request Lifecycle State Machine
Each request goes through these stages in order:
$PREExecution: Executes first. This is where you configure dynamic variables or file I/O (req.attach,req.multipart,req.saveResponseTo,req.cache,req.skip). Current HTTP request body and headers are not yet accessible.- Materialization: The engine scans the raw HTTP text and evaluates inline scripts (e.g.,
${env.HOST}). The output is injected directly into the byte stream. - HTTP Parsing: The materialized byte stream is parsed into a standard Go
http.Request. - Execution (Send): The HTTP request is sent over the network.
$RETRYLoop: Executes immediately after receiving the response. Must return a number: milliseconds to sleep before retrying (jumps back to Step 4). A negative number breaks the loop.$ASSERTExecution: Evaluates the final response to pass or fail the test.$POSTExecution: Used to extract state. Skipped if$ASSERTfailed andfail_fastis true.
Configuration Aggregation (Deep Merging)
When a leaf directory inherits a config.scenario file from a parent, configurations are deep merged.
If a parent sets "req_timeout": "5s" and a child sets "env": {"NEW": "true"}, the resulting scenario will have both settings. If both define the same key, the child's value overwrites the parent's value. Unmentioned default values (like fail_fast) are preserved throughout the merge chain.
AST Caching & Request Collapsing
sinq compiles every Lua block to bytecode (AST) and caches it in memory. The cache key is tied to the physical byte-offset of the script in the file.
If multiple workers process an identical request at the same time, sinq can collapse them with a singleflight mechanism. This is strictly opt-in per request by calling req.cache(true) in the $PRE block. When enabled, the first worker performs the network call, while all other waiting workers receive the cached result instantly when the first finishes.