Skip to main content

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:

  1. 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.
  2. It lives under a skipped directory: anything starting with ., or target, node_modules, dist, build, .venv.
  3. It is behind a symlink. Symlinks are not followed.
  4. 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:

  1. An unknown field. Manifests are parsed with unknown fields denied, so a typo is an error rather than a silently ignored key. Common ones: tag instead of tags, script instead of file, parallel-safe instead of parallel_safe, variable instead of variables.
  2. A missing required field. id and file are the only mandatory keys on a [[test]]; everything else has a default.
  3. A bad duration. timeout needs a unit — "10s", not "10".
  4. 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:

tests/auth.snag.toml
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:

  1. Mark the test parallel_safe = false so it runs alone in phase 1.
  2. Give it its own fixture — a unique id, tenant, or namespace per run.
  3. Reproduce with snag run -j 1 to confirm the diagnosis, and with --seed N to 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.