moonrockzmoonspec
v0.11.0next
You are reading the docs for main (unreleased). Go to the v0.11.0 docs.
Docs / Configuration Reference
On this pageConfig File (moonspec.json5 / moonspec.json)Config FieldsExample: Minimal ConfigExample: Full ConfigRunOptions (Runtime Configuration)Builder MethodsCLI Flagsmoonspec gen testsmoonspec gen stepsmoonspec gen buildmoonspec checkColorsPrecedence OrderMerge BehaviorExampleSchema

Configuration Reference

moonspec supports configuration through config files, runtime options, and CLI flags. These layers compose: config files set project defaults, CLI flags override them for one-off runs, and RunOptions controls runtime behavior programmatically.


Config File (moonspec.json5 / moonspec.json)

moonspec auto-discovers config files in the current working directory:

FileRoleFormat
moonspec.jsonBaseJSON5
moonspec.json5OverrideJSON5

Both files are parsed as JSON5, which means you can use comments, trailing commas, and unquoted keys.

When both files exist, moonspec.json serves as the base configuration and moonspec.json5 provides overrides using merge semantics (see Merge Behavior below). When only one file exists, it is used as-is.

To bypass auto-discovery entirely, pass an explicit path with the --config flag:

moonspec gen tests --config path/to/custom.json5 features/*.feature

Config Fields

{
  // World type name for codegen (e.g. "CalcWorld")
  "world": "CalcWorld",

  // Codegen mode: "per-scenario" (default) or "per-feature"
  "mode": "per-scenario",

  // Execution: "async" (default) or "sync"
  "execution": "sync",

  // Skip tags: Gherkin tags that cause scenarios to be skipped
  // Default: ["@skip", "@ignore"] (handled by RunOptions at runtime)
  "skip_tags": ["@skip", "@ignore", "@wip"],

  // Steps codegen configuration for `moonspec gen steps`
  "steps": {
    "output": "alongside",
    "exclude": ["lib/*", "vendor/*"]
  },

  // Generate tests that print reports without ANSI colors
  "no_color": false,

  // Test file kind for gen tests / gen build: "wbtest" (default) or "test"
  "test_kind": "wbtest"
}

world (string)

The World type name used during code generation. This is the struct that holds scenario state and is passed to every step handler.

Required for legacy same-package gen tests and gen build. Can be set here or via --world. Shared generation uses shared.world_factory instead.

mode (string | object)

Controls how test functions are generated from Gherkin features. Two values are supported:

  • "per-scenario" (default) -- generates one test block per scenario. Each scenario runs independently with its own World instance.

  • "per-feature" -- generates one test block per feature file. All scenarios in the feature share a single test entry point.

The mode field also accepts a per-file map for mixed-mode projects:

{
  "mode": {
    "features/checkout.feature": "per-feature",
    "features/search.feature": "per-scenario",
    "*": "per-scenario"  // fallback for all other files
  }
}

The "*" key acts as a fallback. If a feature file path is not listed and no "*" key exists, the default "per-scenario" mode is used.

execution (string or object)

Select "async" or "sync" for gen tests and gen build. The default is "async". CLI --execution sync|async wins over the merged config value, which wins over that default. This is independent of mode and test_kind; both execution choices support per-feature/per-scenario and white-box/black-box output. Invalid values are rejected.

Sync output uses ordinary test blocks, run_or_fail_sync and the complete original source embedded as FeatureSource::Text. Import the root moonrockz/moonspec and the configured formatter package, without an async test driver. The source URI stays available for diagnostics without a runtime read.

Legacy scalar async output preserves async test, run_or_fail and FeatureSource::File. Import "moonrockz/moonspec/async" @moonspec and retain moonbitlang/async for the test driver. Build rules pin the selected execution. Rerun gen build --apply after a config change to update existing rules.

See sync execution for the 0.12.0 import migration, verified toolchain, target matrix and host limits.

An object opts into target selection with one shared setup:

{
  execution: { default: "async", targets: { "wasm-gc": "sync" } },
  shared: {
    package: "example/multi-target/definitions",
    world_factory: "world",
    options_factory: "options",
  },
}

default is required; targets is optional. Supported target keys are wasm, wasm-gc, js and native. Every target resolves before writes; async on Wasm-GC is invalid and never falls back silently. Unknown keys and values reject. The CLI scalar override replaces the entire policy while retaining shared factories. Both shared modes embed complete original source.

shared (object)

package, world_factory and options_factory are required nonempty strings. Factories must be simple identifiers. The package must belong to the consumer's own module, either its root or a visible subpackage; external shared packages are unsupported. The public World can keep its fields private. Expose its World impl and factories, and return fresh options and stateful sinks on every call. Legacy same-package whitebox output still supports a private World.

An effective execution object requires shared. Partial child configs can inherit it, which the schema permits; the CLI validates the merged result. Both execution and shared replace their whole inherited field. Read one setup across targets for runnable commands, suite overrides, source API migration, ownership and regeneration rules. This feature requires the local native checkout CLI on PATH; all policy --moonx rules reject until a supporting registry release has been verified.

skip_tags (array of strings)

Gherkin tags that cause scenarios to be skipped during execution. Scenarios tagged with any of these tags are skipped without executing step handlers or case/step hooks. Run-level hooks still execute for the enclosing run.

Tags may include a reason using the @skip("reason") syntax. The reason is extracted and attached to the skip status in results.

When omitted from config, the runtime default is ["@skip", "@ignore"].

moonspec gen tests uses the same tags. It puts a #skip("reason") attribute above each generated test whose scenario, feature or Examples block has a skip tag, so moon test reports the scenario as skipped. moon test --include-skipped runs it anyway. In per-feature mode, only a feature-level skip tag adds #skip.

no_color (boolean)

When true, moonspec gen tests generates tests that call options.no_color(true) and create pretty formatters with no_color=true. Default: false. Environment variables can override it at runtime; see Colors.

test_kind (string)

The kind of test file that moonspec gen tests and moonspec gen build write.

  • "wbtest" (default) -- white-box tests, <name>_feature_wbtest.mbt. They can use private items of the package, such as the World impl that moonspec gen steps generates.

  • "test" -- black-box tests, <name>_feature_test.mbt. They see only the package's public API, so the World type and its impl must be pub.

In legacy scalar mode, an -o value ending in .mbt is the exact file name. Shared mode takes an output directory and generates separate packages; wbtest does not expose private items across those packages.

steps (object)

Configuration for the moonspec gen steps command.

FieldTypeDefaultDescription
outputstring"alongside"Output strategy for generated step registration files
excludearray of string(none)Glob patterns for files to exclude from scanning

The output field accepts:

  • "alongside" (default) -- writes <world>_steps_gen.mbt next to the step functions.

  • "per-package" -- writes one moonspec_gen.mbt per MoonBit package.

  • "generated" -- writes to _generated/<world>_steps.mbt at the module root. That directory is not part of the World type's package, and MoonBit allows the World impl only there, so this output does not compile. gen steps prints a warning.

  • Custom path -- any other string is treated as a directory path. It must be the directory of the World type's package.

formatters (array of objects)

Configure output formatters for test results. Each entry specifies a formatter type and its output destination.

{
  "formatters": [
    { "type": "pretty", "output": "stdout" },
    { "type": "junit", "output": "reports/results.xml" },
    { "type": "messages", "output": "stderr" }
  ]
}

Each formatter entry has:

  • type -- "pretty", "junit", or "messages"

  • output -- "stdout", "stderr", or a file path

  • no_color -- (optional, pretty only) disable ANSI colors

Default behavior: When no formatters are configured, moonspec outputs pretty-formatted results to stdout.

Multiple formatters: You can configure multiple formatters simultaneously -- for example, pretty output to the console for local development and JUnit XML to a file for CI.

Programmatic API:

let fmt = @format.PrettyFormatter::new()
options.add_formatter(&fmt, @moonspec.Stdout)

let junit = @format.JUnitFormatter::new()
options.add_formatter(&junit, @moonspec.File("reports/results.xml"))

// Remove all formatters
options.clear_sinks()

Example: Minimal Config

{
  "world": "CalcWorld"
}

Example: Full Config

{
  "world": "EcomWorld",
  "mode": {
    "features/checkout.feature": "per-feature",
    "*": "per-scenario"
  },
  "skip_tags": ["@skip", "@ignore", "@wip"],
  "steps": {
    "output": "alongside",
    "exclude": ["vendor/*"]
  }
}

RunOptions (Runtime Configuration)

RunOptions controls how the runner executes features at runtime. It is created in your test code and passed to the runner.

let opts = RunOptions::new(features)
opts.parallel(true)
opts.max_concurrent(8)
opts.retries(2)
opts.tag_expr("@smoke and not @slow")
opts.skip_tags(["@skip", "@ignore", "@wip"])
opts.dry_run(false)

Builder Methods

parallel(value : Bool)

Enable or disable parallel scenario execution. Default: false.

For async execution, scenarios run concurrently up to max_concurrent. Each gets its own World. Sync execution rejects parallel(true) with SyncExecutionError::ParallelNotSupported before World factories, hooks or events.

max_concurrent(value : Int)

Set the maximum number of scenarios that can run concurrently during parallel execution. Default: 4.

Only takes effect for async execution when parallel is true.

retries(value : Int)

Set the global retry count for failed scenarios. Default: 0.

When a scenario fails, it is re-executed up to value additional times. A fresh World instance is created for each attempt. Only the final attempt's result counts toward the run summary.

Per-scenario @retry(N) tags override this global setting. Negative values are clamped to 0. Sync and async share the same failed-only retry loop; Undefined, Pending and Skipped scenarios do not retry. Dry-run never retries.

tag_expr(value : String)

Set a boolean tag expression to filter which scenarios run. Default: "" (no filter).

Supports boolean operators:

"@smoke and not @slow"
"@checkout or @cart"
"not @wip"

scenario_name(value : String)

Filter scenarios by name. Default: "" (no filter).

Only scenarios whose name contains the given string will run.

dry_run(value : Bool)

Enable or disable dry-run mode. Default: false.

When enabled, steps are matched against definitions but handlers are not executed. Case/step hooks are skipped; before/after TestRun hooks still execute. Matched steps report as Skipped("dry run") and undefined steps remain Undefined. Use it to validate step wiring; run hooks can still have side effects.

skip_tags(tags : Array[String])

Set the tags that cause scenarios to be skipped. Default: ["@skip", "@ignore"].

Replaces the entire skip tag list. Scenarios with any of these tags are skipped without executing step handlers or case/step hooks. Run-level hooks still execute for the enclosing run.

no_color(value : Bool)

Turn off ANSI colors in the run_or_fail failure report. Default: false. Environment variables can override it at runtime; see Colors.

add_sink(sink : &MessageSink)

Add a message sink for envelope output. Sinks receive structured messages as the run progresses. Multiple sinks can be added.

add_formatter(sink : &MessageSink, dest : OutputDest)

Register a formatter with an output destination. The formatter receives messages during the run, and its output is automatically written to the destination when the run completes.

Destinations: @moonspec.OutputDest::Stdout, @moonspec.OutputDest::Stderr, @moonspec.OutputDest::File(path). File/stderr writes require host support. Sync failures raise SyncExecutionError::HostIoFailed(operation~, path~, message~); async retains best-effort output. Text and Parsed avoid source reads, while File input remains host-dependent. Raw Wasm/browser hosts must supply the standard MoonBit ABI; this does not promise a browser filesystem.

clear_sinks()

Remove all registered sinks and formatters.


CLI Flags

CLI flags override the corresponding config file fields for the current invocation.

moonspec gen tests

FlagShortOverrides ConfigDescription
--world-wworldWorld type name (e.g. CalcWorld)
--mode-mmodeCodegen mode: per-scenario or per-feature
--config-c(all)Explicit config file path
--output-dir-o--Output directory for generated test files
--no-colorno_colorGenerate tests without ANSI colors
--executionexecutionasync (default) or sync, independent of mode/kind
--test-kindtest_kindwbtest (default) or test
--preview--Preview shared generation without writing

When --world, --mode, --execution or --test-kind is provided, the CLI value takes precedence over the config file value. When --config is provided, auto-discovery is bypassed entirely and only the specified file is loaded.

Note: --mode on the CLI applies uniformly to all files in that invocation. To use per-file mode mapping, configure it in the config file instead.

moonspec gen steps

FlagShortDescription
--config-cExplicit config file path
--dir-dDirectory to scan for step definitions

moonspec gen build

FlagShortOverrides ConfigDescription
--world-wworldWorld type name (e.g. CalcWorld)
--package-p--Package directory that gets the tests (default: src)
--config-c(all)Explicit config file path
--apply--Write missing entries to <package>/moon.pkg
--executionexecutionasync (default) or sync, independent of mode/kind
--test-kindtest_kindwbtest (default) or test
--mode-mmodeShared generation grouping
--no-colorno_colorShared generation color setting
--refresh-output--Repeatable validated planned source path, used by generated shared rules
--moonx--Rule runs moonx moonrockz/moonspec/cmd/moonspec@<CLI version> instead of moonspec

moonspec check

The check subcommand accepts positional .feature file arguments and does not use config file settings.

Shared gen build --apply writes owned moonspec_gen packages and a manifest, leaving parent package metadata intact. Feature-body and existing grouping changes regenerate through Moon. Rerun gen build --apply for structural target/package/feature-list/test-kind changes or config-path additions/removals. Manual unchanged generation performs no writes. Rules use the current local native CLI on PATH and validated refresh paths; do not edit generated files.


Colors

The run_or_fail failure report and PrettyFormatter use ANSI colors. Turn them off when you build the tests:

  • moonspec.json5: "no_color": true

  • CLI: moonspec gen tests --no-color

  • Code: options.no_color(true) or @format.PrettyFormatter::new(no_color=true)

Environment variables override the compiled value when the tests run:

VariableEffect
MOONSPEC_COLOR=alwaysColor on, even when no_color is set
MOONSPEC_COLOR=neverColor off
NO_COLOR=<any non-empty value>Color off, unless MOONSPEC_COLOR=always (no-color.org)

Editor test panels that read moon test --test-failure-json may show ANSI codes as raw text. Set NO_COLOR=1 for those runs.

Precedence Order

Configuration values are resolved with the following precedence (highest wins):

  1. CLI flags -- --world, --mode, etc.

  2. Nearer package config -- for generation, files toward the selected output package override ancestors

  3. Per-directory configs -- moonspec.json5 overrides moonspec.json

  4. Built-in defaults -- mode: "per-scenario", skip_tags: ["@skip", "@ignore"], etc.

Discovery starts with the module-root configuration for generation. When --config is used, steps 2 and 3 are replaced by the single specified file.


Merge Behavior

When both moonspec.json and moonspec.json5 exist, they are merged using field-level override semantics:

  • Each top-level field in the override config (moonspec.json5) replaces the corresponding field in the base config (moonspec.json) when present.

  • Fields present in the base but absent from the override are preserved.

  • Fields present in the override but absent from the base are added.

  • For nested objects like steps, the entire object is replaced -- individual sub-fields are not merged independently.

  • The same rule applies to execution and shared, including the entire target map. A missing field inherits.

  • For array fields like skip_tags, the override array replaces the base array entirely (arrays are not concatenated).

Example

moonspec.json (base):

{
  "world": "AppWorld",
  "mode": "per-scenario",
  "skip_tags": ["@skip", "@ignore"]
}

moonspec.json5 (override):

{
  "mode": "per-feature",
  "skip_tags": ["@skip", "@ignore", "@wip"]
}

Resolved config:

{
  "world": "AppWorld",           // preserved from base
  "mode": "per-feature",         // replaced by override
  "skip_tags": ["@skip", "@ignore", "@wip"]  // replaced by override
}

Schema

A JSON Schema is available at schemas/moonspec.schema.yaml in the moonspec repository. Use it for IDE validation and autocompletion in editors that support YAML/JSON schema references.

For VS Code, you can add a reference in your workspace settings:

{
  "json.schemas": [
    {
      "fileMatch": ["moonspec.json", "moonspec.json5"],
      "url": "./schemas/moonspec.schema.yaml"
    }
  ]
}
Edit this page on GitHub ↗