Getting Started
Early release (0.x) -- API may evolve before 1.0.
Requirements
- Bun >= 1.0.0 (runtime and package manager)
- Shell: bash / POSIX shell (macOS, Linux; Windows via WSL)
Security note: mdspec executes shell commands from markdown blocks. Do not run it on untrusted content.
Installation
bun add -d mdspecnpm install -D mdspecpnpm add -D mdspecyarn add -D mdspecNote: All package managers can install mdspec, but Bun is required to run it. Use
bunx mdspecorbun mdspecto execute tests.
Write a Test
Create a file called example.spec.md:
# My CLI Tests
```console
$ echo "Hello, mdspec!"
Hello, mdspec!
```
```console
$ date +"%Y"
/\d{4}/
```Each console code fence is a test block. Lines starting with $ are commands; everything else is expected output.
Set Up and Tear Down Once
Each spec file already starts in a fresh temporary directory. Define beforeAll and afterAll lifecycle fences when the examples need a shared fixture or another resource with an explicit lifetime. Their bodies are raw shell, without console-style $ prompts:
## Setup
```beforeAll reset
export FIXTURE_DIR="$(mktemp -d)"
cd "$FIXTURE_DIR"
printf 'ready\n' > status.txt
```
```afterAll
rm -rf "$FIXTURE_DIR"
```
## Example
```console
$ cat status.txt
ready
```reset clears accumulated shell state before setup and returns to the spec's temporary working directory. The new shell still inherits mdspec's process environment, so explicitly export empty values for any ambient application variables that would make a fixture target live services. mdspec runs beforeAll once before the test blocks and guarantees afterAll after the last block even when setup or a test fails. Lifecycle fences are declarations, not tests, so they do not appear in the executable block count.
Environment variables, the working directory, and shell functions created by the setup hook persist between blocks. $ROOT points back to the source tree when a fixture needs project files.
Use a console reset fence when a later example must discard accumulated shell state and return to the spec's original temporary directory:
```console reset
$ echo "${FIXTURE_DIR:-unset}"
unset
```Resetting shell context does not stop external processes or remove temporary directories; lifecycle resources still belong in afterAll.
Run It
mdspec example.spec.mdOutput is markdown-formatted with pass/fail indicators, colored diffs, and headings from your document structure.
Glob Patterns
mdspec tests/**/*.spec.mdSnapshot Updates
When expected output changes, update it automatically:
mdspec --update tests/**/*.spec.mdThe markdown file is rewritten in place with the actual output replacing the expected output.
Vitest Integration
Run markdown tests through Vitest alongside your TypeScript test suite.
Setup: Create a test file (e.g., tests/md.test.ts):
import { registerMdTests } from "mdspec/vitest"
await registerMdTests("tests/**/*.spec.md")Run:
bunx --bun vitest run tests/md.test.tsThis gives you Vitest's reporters, --watch mode, --coverage, and --bail integration.
Note: Vitest integration still uses Bun under the hood for test execution.
Debug Mode
Enable debug output with the DEBUG environment variable:
DEBUG='mdspec:*' mdspec tests/example.spec.mdAvailable namespaces:
| Namespace | What it shows |
|---|---|
mdspec:runner | Test file discovery, parsing, and execution |
mdspec:files | Helper file creation from file= blocks |
mdspec:session | Session state management (env, cwd, functions) |
How It Works
- Parse -- Markdown is parsed with remark, extracting
consolecode fences - Isolate -- Each test file runs in a fresh temp directory (
$ROOTpoints back to the source tree) - Execute -- Commands run individually with state persisted between them via temp files
- Match -- Actual output is compared against expected output using pattern matching
- Report -- Results are printed as markdown with diffs for failures