aux4 testing tool
aux4/test provides a lightweight markdown-driven test runner for aux4 packages. Tests are written as Markdown files with special fenced blocks (execute, expect, error, file, timeout, hooks, etc.). The package interprets these files and runs commands, capturing stdout/stderr and validating output against the provided expectations. It's designed for authoring reproducible command-line tests, verifying CLI behavior, file artefacts, timeouts, and test hooks.
This README documents all features exercised by the official test suite. Each section shows real test-file content (copied from the test/ folder) and explains the behavior and usage. Examples are taken verbatim from the repository tests so you can copy and run them.
aux4 aux4 pkger install aux4/test
This package requires system dependencies. You need to have one of the following system installers:
For more details, see system-installer.
Run the test runner in the current directory:
aux4 test run .
This scans for .test.md files in the directory structure and runs them using the aux4 test runner. By default the runner uses the directory "."; you may pass another directory as the positional dir variable.
The package provides these primary commands:
See command docs: aux4 test run, aux4 test coverage, aux4 test report, and aux4 test add.
---
Overview:
execute blocks contain shell commands to run.expect blocks validate stdout (exact match by default).error blocks validate stderr.execute block with one or more expect/error blocks.Example (from test/execute-expect.test.md):
# Execute/Expect Basic Functionality
Tests for basic execute and expect blocks to verify command execution and output validation.
## Simple Echo Test
echo "Hello World"
Hello World
How it works:
expect block is matched against stdout; error is matched against stderr.expect requires the output lines to match exactly (including order and line breaks).Other examples in the same file illustrate multi-line output, combined stdout/stderr, and environment variable usage.
---
The runner supports modifiers appended to the expect/error block token to change matching semantics.
General forms:
- ```expect:ignoreCase — case-insensitive exact matching
- ```expect:json — JSON formatting (pretty-print compact JSON before comparing)
expect:regex:ignoreCase are supported. The :json modifier can also be combined with others: `expect:json:partial, `expect:json:ignoreCase, ``expect:json:regex.Below are examples directly taken from the tests for each modifier.
Expect:partial (substring and wildcard matching)
# Expect Partial Modifier
Tests for expect:partial modifier to verify substring matching and wildcard pattern matching.
## Simple Substring Match
echo "This is a long output with many words"
long output
Key points:
partial checks that the expected text occurs somewhere in stdout.*? acts like a "match anything" placeholder in partial patterns. For example Start *? end will match Start middle end.* acts like a greedy "match anything" placeholder.** acts like a multiline "match anything" placeholder that can span across newlines. For example Start**end will match Start\nMiddle content\nend.partial also applies to error:partial for stderr.Expect:ignoreCase (case-insensitive matching)
# Expect IgnoreCase Modifier
Tests for expect:ignoreCase modifier to verify case-insensitive output matching.
## Basic Case Insensitive Match
echo "Hello World"
hello world
Key points:
ignoreCase makes the comparison case-insensitive.expect (stdout) and error (stderr).regex so expect:regex:ignoreCase runs a case-insensitive regex.Expect:regex (regular expressions)
# Expect Regex Modifier
Tests for expect:regex modifier to verify regular expression matching in output validation.
## Simple Pattern Matching
echo "Hello World 123"
^Hello World \d+$
Key points:
error:regex validates stderr with a regex.Expect:json (JSON formatting)
The :json modifier pretty-prints compact JSON output before comparison. When a command outputs inline/compact JSON like {"name":"John","age":30}, the modifier parses it and reformats it with JSON.stringify(JSON.parse(value), null, 2) so the expect block can be written in a human-readable, indented format.
The JSON formatting is applied to the actual output (stdout or stderr) before any other modifier logic runs. This means :json can be combined with other modifiers:
expect:json — exact match against pretty-printed JSONexpect:json:partial — substring/wildcard match against pretty-printed JSONexpect:json:ignoreCase — case-insensitive match against pretty-printed JSONexpect:json:regex — regex match against pretty-printed JSONerror:json — same behavior for stderrIf the output is not valid JSON, it is left as-is and the test will fail with a meaningful diff showing what was expected vs. the raw output.
# Expect JSON Modifier
## Basic JSON Object
echo '{"name":"John","age":30}'
{ "name": "John", "age": 30 }
## JSON Combined with Partial
echo '{"status":"ok","data":{"count":42,"items":["x","y"]}}'
"count": 42
## Error JSON
echo '{"error":"not found","code":404}' >&2
{ "error": "not found", "code": 404 }
---
Overview:
execute block may be followed by many expect blocks.Example (from test/multiple-expects-test.test.md):
# Multiple Expects Test
Test that a single execute command can have multiple expect blocks with different modifiers.
## Test Multiple Expects for Single Execute
echo "Line 1: Hello World Line 2: Testing 123 Line 3: Final result"
Hello World
Testing 123
Final result
Line \d+: Hello World
How it works:
expect (or error) block is evaluated independently.---
Overview:
file:<filename> blocks create files with the provided content during the test.file blocks are scoped: they are available to nested tests, but siblings do not share file state.Example (from test/file-scope.test.md):
# File Block Scope Testing
Tests to verify that files created in ```file blocks are available for nested tests but not for tests at the same level.
## Parent Scenario with File
This file is created at parent level
### Nested Test 1 - File Should Be Available
cat parent-file.txt
This file is created at parent level
Important scope rules illustrated by the test:
file declared at a parent level is available to nested scenarios/tests beneath it.Example showing sibling isolation (from same test file):
This scenario is at the same level as "Parent Scenario with File", so the parent-file.txt should not be available here.
ls parent-file.txt 2>/dev/null || echo "File not found"
File not found
---
Overview:
beforeAll — run once before the tests in the current scenario.afterAll — run once after all tests in the current scenario.beforeEach — run before each test in the current scenario.afterEach — run after each test in the current scenario.beforeAll and afterAll. The runner supports the other hooks as well; they follow the same scoping rules as file blocks.Example with beforeAll/afterAll (from test/hooks.test.md):
# Hooks Functionality
Tests for beforeAll, afterAll, beforeEach, and afterEach hooks to verify test setup and cleanup.
## Setup and Cleanup Hooks
mkdir -p test-dir echo "Setup complete" > test-dir/setup.log
rm -rf test-dir
### Test 1 - Verify Setup
cat test-dir/setup.log
Setup complete
How it works:
beforeAll created the directory and file; nested tests verify its presence.afterAll removes the created artifacts.beforeEach and afterEach (not used in the example) would run around each test in the scenario, enabling per-test setup/cleanup (e.g., resetting state or deleting temporary files).---
Overview:
timeout block containing a number (milliseconds) to override the default test timeout for the following execute block.Examples (from test/timeout.test.md):
# Timeout Functionality
Tests for timeout configuration to verify that long-running tests can be properly configured.
## Fast Command Test
1000
echo "Quick command"
Quick command
More examples in the same file show 5000 and 10000 millisecond timeouts. If no timeout block is present, the runner uses the default timeout.
Practical notes:
timeout block directly before the execute it applies to.---
error blocks validate stderr content (supports the same modifiers as expect, e.g., error:partial, error:regex, error:ignoreCase).expect and error blocks are used for the same execute.Example snippet (from execute-expect.test.md):
## Combined Output and Error
echo "Success output" && echo "Error output" >&2
Success output
Error output
Key points:
expect and error blocks respectively.---
The package includes aux4 test add to programmatically append tests to a test markdown file. The test suite includes tests that exercise adding tests with files and different heading levels.
Excerpt (from test/add.test.md):
# Aux4 Test Add Command
Tests for the `aux4 test add` command functionality to verify test creation and file generation.
echo "# Test Suite" > my-test-suite.test.md
rm -f my-test-suite.test.md sample-file.txt another-file.js
## Basic Test Addition
### Add Simple Test with Execute
echo "# Test Suite" > my-test-suite.test.md && aux4 test add my-test-suite.test.md --level 2 --name "Simple Echo Test" --execute "echo Hello World"
cat my-test-suite.test.md
echo Hello World
Hello World
Usage:
aux4 test add <testFile> --level <n> --name "<name>" --execute "<command>" appends a test at the specified Markdown heading level.--file <path> may be included multiple times to embed file blocks into the test being added.The add.test.md file includes multiple examples: adding tests with single or multiple files, different heading levels, and execute-only tests. The test verifies that the add command correctly writes headers, file blocks, execute blocks, and expect blocks into the test file.
---
Below are runnable, real examples taken directly from the tests. Each example includes the test file heading and the important sections so you can see the full test structure.
Example: Basic execute/expect test file fragment
# Execute/Expect Basic Functionality
## Simple Echo Test
echo "Hello World"
Hello World
Example: Partial matching with wildcards
# Expect Partial Modifier
## Wildcard Pattern with \*?
echo "Start middle end"
Start *? end
Example: Case-insensitive expect and combining with regex
# Expect IgnoreCase Modifier
## Combined Modifiers - Regex and IgnoreCase
echo "Error Code: ABC123"
error code: [a-z]+\d+
Example: File scoping (parent creates a file, nested tests use it)
# File Block Scope Testing
## Parent Scenario with File
This file is created at parent level
### Nested Test 1 - File Should Be Available
cat parent-file.txt
This file is created at parent level
Example: Hooks setup/teardown
# Hooks Functionality
## Setup and Cleanup Hooks
mkdir -p test-dir echo "Setup complete" > test-dir/setup.log
rm -rf test-dir
### Test 1 - Verify Setup
cat test-dir/setup.log
Setup complete
Example: Timeout usage
# Timeout Functionality
## Long Timeout Test
10000
sleep 2 && echo "Command completed after 2 seconds"
Command completed after 2 seconds
Example: Adding a test using aux4 test add and verifying the produced test file (excerpt)
# Aux4 Test Add Command
echo "# Test Suite" > my-test-suite.test.md && aux4 test add my-test-suite.test.md --level 2 --name "Simple Echo Test" --execute "echo Hello World"
cat my-test-suite.test.md
echo Hello World
Hello World
---
The :similar modifier compares the command output against a reference using text similarity metrics. No LLM calls — fully deterministic with zero token cost.
Two forms for providing the reference:
Inline reference (content below --- separator):
echo "Hello World"
metric: fuzzy pass: 0.8 --- Hello World
File reference:
Hello World
echo "Hello World!"
pass: 0.9 file: expected/output.txt
Block fields:
name — optional label for the score (appears in JSON output)metric — similarity algorithm: fuzzy (default), cosine, jaccardpass — minimum similarity score 0-1 (default: 0.8)file — path to reference file (alternative to inline content)Available metrics:
fuzzy — Levenshtein distance ratio (0-1). Character-level similarity. Good for "almost the same" text.cosine — word-level cosine similarity (0-1). Good for meaning overlap regardless of word order.jaccard — word set intersection/union (0-1). Good for "same words present."Can be combined with :ignoreCase:
pass: 0.9 --- hello world
Output:
similarity: 0.92 (fuzzy) ✓ PASS (>= 0.8)
---
The :ai:score modifier uses an LLM to score the command output on a criterion, returning a numeric score instead of pass/fail. Requires --aiConfig and --configFile like expect:ai.
cat config.yaml
name: correctness eval: Does the output match expected/config.yaml? range: 1-5 pass: 3
name: completeness eval: Are all required fields present?
Block fields:
name — optional label for the scoreeval — what to evaluate (required, the judge prompt)range — score range as min-max (default: 1-5)pass — minimum passing score (default: 3)The judge agent has readFile tool access, so the eval text can reference files created by file: blocks (e.g., "Does the output match expected/config.yaml?").
Output:
correctness: 5/5 Values match the ground truth
completeness: 4/5 Missing optional headers
✓ PASS (all criteria met)
Run with:
aux4 test run test.md --aiConfig agent --configFile config.yaml
---
Write structured test results to a JSON file:
aux4 test run test/ --output results.json
The JSON includes all test results with scores and assertion outcomes:
{
"timestamp": "2026-04-25T10:30:00Z",
"tests": [
{
"title": "should create valid config",
"passed": true,
"duration": 8400,
"results": [
{ "type": "exact", "passed": true },
{ "type": "similar", "name": "structure", "metric": "fuzzy", "score": 0.92, "pass": 0.8 },
{ "type": "ai:score", "name": "correctness", "eval": "Does it match?", "score": 5, "max": 5, "pass": 3, "reason": "Values match" }
]
}
],
"summary": {
"total": 1,
"passed": 1,
"failed": 0
}
}
---
aux4/test includes a coverage system that tracks which execute steps, branches, and loop iterations were exercised during test runs or regular command usage.
aux4 test coverage test/
This runs all tests normally, then prints a coverage report showing which commands and execute steps were hit:
Coverage Report
======================================================================
Package: my-app@1.0.0
main/build 3/3 steps ████████████ 100% 1.2s
main/deploy 2/5 steps █████░░░░░░░ 40% 0.3s
✗ [2] when:${env}=prod:nout:backup-db
✗ [3] nout:notify-slack ${env}
✗ [4] log:deployed to ${env}
----------------------------------------------------------------------
Summary:
Commands: 2/3 (67%)
Steps: 5/8 (63%)
Branches: 1/2 (50%)
Slowest commands:
main/build 1.2s
main/deploy 0.3s
======================================================================
| Metric | What it tracks | |--------|---------------| | Step coverage | Which execute items in each command were hit | | Command coverage | Which commands were invoked at least once | | Branch coverage | Which when: conditions were evaluated as both true and false | | Iteration tracking | How many times each: loops ran and per-iteration durations | | Duration | Time per step, command, and iteration |
Coverage instrumentation is built into the aux4 core. Set the AUX4_COVERAGE_FILE environment variable to record coverage from any aux4 command:
# Record coverage from regular commands
AUX4_COVERAGE_FILE=cov.json aux4 build
AUX4_COVERAGE_FILE=cov.json aux4 deploy --env staging
# Multiple runs merge into the same file
AUX4_COVERAGE_FILE=cov.json aux4 deploy --env prod
# View the report
aux4 test report cov.json
When AUX4_COVERAGE_FILE is not set, coverage is completely disabled with zero performance overhead.
Use --threshold to fail with exit code 1 if step coverage is below the specified percentage:
# Fail CI if coverage drops below 80%
aux4 test coverage test/ --threshold 80
# Same for standalone report
aux4 test report cov.json --threshold 80
When below threshold:
Coverage 50% is below threshold 80%
aux4 test report <coverage-file> [--dir <path>] [--threshold <n>]
The report command reads a coverage JSON file and scans the specified directory for .aux4 files to build the full universe of commands, then renders which were covered and which were missed.
See command docs: aux4 test coverage and aux4 test report.
---
The dataset block runs an entire scenario (including nested children) once per entry in a JSON array, similar to Jest's it.each. Variables from each dataset entry are substituted into execute, expect, error, file, and hook blocks using {{variable}} syntax.
Two syntax forms are supported:
Config block (with options):
## should add numbers
file: dataset/math.json root: $.data.items key: id
echo $(({{a}} + {{b}}))
{{result}}
Shorthand (file path in the language tag):
## should add numbers
echo $(({{a}} + {{b}}))
{{result}}
Where dataset/math.json contains:
[
{"a": 1, "b": 2, "result": "3"},
{"a": 10, "b": 20, "result": "30"}
]
file — path to a JSON file (resolved relative to the test file's directory)root — JSONPath expression to extract an array from the JSON structure (default: $, the root). Use when the array is nested inside the JSON, e.g., $.data.itemskey — field name from each entry to use as the label in test output. When set, entries appear as [keyValue] instead of [#0], [#1], etc.dataset block is placed at a scenario heading level (e.g., ##). The entire describe for that scenario — including all its tests and nested children — is repeated once per dataset entry.--config). Dataset values override config values when keys collide.JSON.stringify'd for substitution.null/undefined values become empty strings.root resolution) produces an error.When a child scenario also has a dataset block, the result is a cartesian product — every combination runs:
## should combine greetings
file: dataset/prefixes.json
### should greet
file: dataset/names.json
echo "{{prefix}}, {{name}}!"
{{prefix}}, {{name}}!
With prefixes.json = [{"prefix":"Hello"},{"prefix":"Hi"}] and names.json = [{"name":"Alice"},{"name":"Bob"}], this runs 4 tests (2 x 2).
The child dataset entries are merged on top of the parent's, so inner variables can override outer ones.
Each dataset entry appears as a separate describe block in test output:
1.1. should add numbers [#0]
✓ 1. should print output
1.1. should add numbers [#1]
✓ 1. should print output
With key: id:
1.1. should add numbers [addition]
✓ 1. should print output
1.1. should add numbers [subtraction-like]
✓ 1. should print output
beforeAll, afterAll, beforeEach, and afterEach hooks inside a dataset scenario run per entry — each entry gets its own describe block, so beforeAll runs once per entry, not once globally.
---
file: blocks automatically create parent directories if they don't exist, and clean up only the directories that were created:
host: localhost port: 3000
cat nested/deep/config.yaml
host: localhost port: 3000
---
file:<filename> blocks to create fixture files. Place them in the scenario where they should be visible (parent level for nested tests to access).execute blocks and multiple expect blocks to validate different aspects of output.expect:partial for flexible substring tests, expect:regex for pattern matching, and expect:ignoreCase for case-insensitive comparisons.timeout only when necessary for long-running commands to avoid hanging test suites.beforeAll and afterAll for expensive setup/cleanup that applies to many tests; use beforeEach/afterEach for per-test isolation.---
The provided test files in this package demonstrate all primary features:
You can open each file to see complete, runnable test fragments. The README examples above copy the real tests so you can replicate them.
---
expect fails, check which block failed and compare the captured stdout/stderr to the expected content.expect:partial for tests where output contains timestamps, order-insensitive fragments, or variable content.expect:regex with anchors and groups to precisely target the necessary parts.file: block is declared and that scoping matches the intended nested tests.aux4 test add and then inspect the generated .test.md file.---
This package is licensed under the Apache-2.0 License.
See LICENSE for details.