Troubleshooting
Errors are grouped by where they come from: the command line, discovery, the manifest, the script, or the network.
Command line
error: unexpected argument '-t' found
$ snag -t smoke
error: unexpected argument '-t' found
-t, -k, -e, -j, --retries, --timeout, --fail-fast, --dry-run,
and --report belong to a verb. The run verb is inserted automatically
only when the first non-flag argument is a path, so a flags-only invocation has
nothing to trigger it.
snag run -t smoke # name the verb
snag . -t smoke # or give a path
Global flags (-f/--format, --color, -v, -q) work in either position.
A glob matched nothing, or the wrong thing
snag 'tests/*.snag.toml' # quoted: Snag expands it
snag tests/*.snag.toml # unquoted: your shell expands it first
Quote the pattern. An unquoted pattern that matches nothing is passed through
literally by most shells, producing path does not exist.
Discovery
no suite files found
Error: no suite files found (looked for suite.toml / snag.toml / *.snag.toml under the current directory). Run `snag init` to create one.
Exit code 2. Causes, in order of likelihood:
- The file is not named
suite.toml,snag.toml, or*.snag.toml. Rename it, or name it explicitly on the command line — that bypasses the convention. - It lives under a skipped directory: anything starting with
., ortarget,node_modules,dist,build,.venv. - It is behind a symlink. Symlinks are not followed.
- You are in the wrong directory.
snag list . # what does discovery actually see?
snag list weird-name.toml # bypass the naming convention
path does not exist: …
The literal path was not found. Watch for a shell-expanded glob that matched nothing, or a path relative to the repository root while your shell sits somewhere else.
Manifest
Error: parsing suite …
$ snag list
Error: parsing suite ./suite.toml
Exit code 2. Every manifest problem surfaces with this one line — the underlying TOML error is not printed today, so the cause has to be narrowed by hand. In order of likelihood:
- An unknown field. Manifests are parsed with unknown fields denied, so a
typo is an error rather than a silently ignored key. Common ones:
taginstead oftags,scriptinstead offile,parallel-safeinstead ofparallel_safe,variableinstead ofvariables. - A missing required field.
idandfileare the only mandatory keys on a[[test]]; everything else has a default. - A bad duration.
timeoutneeds a unit —"10s", not"10". - Invalid TOML. Check the file with any TOML parser, e.g.
python3 -c 'import tomllib,sys; tomllib.load(open(sys.argv[1],"rb"))' suite.toml.
Bisecting also works: comment out [[test]] blocks until snag list succeeds.
duplicate test id 'x' in …
Ids must be unique within a file. They may repeat across files — the qualified id keeps them apart.
Durations
Durations use humantime syntax and need a unit:
"10s", "500ms", "2m". A bare "10" fails the manifest parse.
A [test.variables] block attached to the wrong test
[[test]]
id = "a"
file = "./a.snag"
[test.variables] # belongs to test "a" — the one directly above
base_url = "https://a"
[[test]]
id = "b"
file = "./b.snag"
TOML tables attach to the most recent [[test]]. Keep each block immediately
under its test, and let snag list --long confirm what was parsed.
Scripts
script … does not exist
The test fails (it does not abort the run) with the path Snag resolved.
file is relative to the manifest, not to your shell:
file = "./auth/login.snag" # → tests/auth/login.snag
snag list --long prints the resolved script path for every test.
Variable not found: base_url
The script used an identifier that no variable defines. Either add it to
[variables] (suite-wide) or [test.variables] (one test), or read it from the
environment with env_or("NAME", "default").
Cannot modify constant base_url
Manifest variables are pushed into scope as constants. Copy before mutating:
let host = base_url; // now it is yours
if env_or("STAGE", "") == "prod" { host = "https://api.example.com"; }
This is a runtime error, not a compile error — snag check will not catch it.
Function not found: assert_stauts (i64, i64)
A typo, or a function that does not exist. Rhai resolves function names at
call time, so this surfaces when the test runs — snag check compiles the
script but cannot catch it. The signature in the message shows the argument
types Snag tried to match, which is also how you spot passing a string where an
integer was expected.
snag check does catch syntax errors, before any request goes out:
$ snag check
error: ./suite.toml::a: Unexpected ';' (line 1, position 9) (a.snag)
Error: 1 of 1 test(s) failed to compile
response body is not valid JSON: …
res.json() was called on a body that is not JSON — very often an HTML error
page from a proxy. Look at what actually came back:
print(`status ${res.status}`);
print(res.text);
assert_ok(res); // assert the status *before* decoding
Asserting the status first turns "not valid JSON" into the far more useful "expected status 200, got 502".
no key 'x' in path 'a.b.x' / index 3 out of range in path …
field() reports the exact segment that failed. Print the decoded body to see
its real shape:
print(res.text);
Remember numeric segments index arrays: field(body, "items.0.sku").
cannot descend into 'x': value is not an object or array
The path ran into a scalar partway down — usually one level too deep, or an object where the API returns an array.
An assert_eq that "should" match
assert_eq is registered per scalar type: integer, float, bool, string. There
is no map or array overload, and no implicit numeric conversion between integer
and float. Compare the fields you care about:
assert_eq(field(body, "total"), 3); // integer
assert_eq(field(body, "ratio"), 0.5); // float
assert_eq(field(body, "name"), "snag"); // string
Network and timing
request failed: …
The request never completed: DNS failure, connection refused, TLS error, or the
client timeout firing. The underlying reqwest error is included — read the
tail of it, that is where the cause is.
In a container, error trying to connect: … certificate almost always means
ca-certificates is missing from the image.
timed out after 10.00s
The test hit its deadline. Which deadline, in precedence order: --timeout,
then the test's timeout, then the suite's. Raise the right one, or find out
why the endpoint got slower:
print(`took ${res.duration_ms}ms`);
A timeout can overshoot slightly on a tight compute loop — the interpreter checks the clock every 2,000 operations — but never on a blocked socket.
A test passes alone and fails in parallel
Shared state. Options, cheapest first:
- Mark the test
parallel_safe = falseso it runs alone in phase 1. - Give it its own fixture — a unique id, tenant, or namespace per run.
- Reproduce with
snag run -j 1to confirm the diagnosis, and with--seed Nto check for order-dependence.
A test is flaky
--retries N keeps the pipeline moving and records the attempt count:
FAIL a [8ms] (after 3 attempts)
Watch attempts in the JSON report over time. A test that regularly needs a
second attempt is reporting a real problem — in the API, or in the test's
assumptions about timing.
Output
Colors are mangled in CI logs
snag tests/ --color never
Auto-detection assumes ANSI support when stdout is a TTY, which some CI environments report inaccurately.
The report file is empty or missing
json and junit buffer everything and write in run_finished, so a run killed
mid-flight leaves nothing. Use jsonl, which flushes per test, when you need
partial results from an interrupted run.
cannot write report to …
The parent directory does not exist, or is not writable. Snag creates the file, not the directory. This is exit code 2, checked before any test runs.
Getting more detail
snag list --long # what discovery resolved, per test
snag check -v # per-test compile results
snag run -j 1 -k "one test" # isolate, serially
snag run -v # captured output for passing tests too
snag . -f jsonl | jq # the machine view of the same run
Still stuck? Open an issue at
github.com/ShortyPing/snag with the
manifest, the script, and the output of snag list --long.