Quick reference for writing YAML scenario files that work with the Wanderland::Scenario loader, ShapeMatcher verifier, and Game engine.
name: "Human-readable scenario name"
description: |
What this scenario tests and why.
Multi-line is fine.
operation: boundary_name # maps to a registered Wanderland::Boundary
level:
world: 1 # grouping (1-based)
number: 1 # ordering within world
hint: "A hint for game mode"
input: # passed to the boundary's call(input)
params:
repo: kremis
kind: method
name: execute
context: # optional — simulates upstream chain output
repo_path: /mnt/repos/kremis
grammars: [ruby]
expected: # verified against boundary output via ShapeMatcher
symbols:
count: 1
first:
name: execute
kind: method
blanks: # optional — for game mode (fill-in-the-blank)
symbol_name:
path: "symbols.0.name"
answer: "execute"
hint: "The method that runs a pipeline stage"
file_path:
path: "symbols.0.path"
answer: "lib/kremis/pipeline.rb"
hint: "Where does the pipeline live?"
The expected section supports exact values and matcher keys. Matchers are special hash keys that trigger comparison logic instead of literal matching.
Scalar values compare directly. Strings, numbers, booleans, null.
expected:
status: ok
count: 42
enabled: true
expected:
files:
count: 17 # exactly 17
annotations:
gte: 1 # at least 1
lte: 100 # at most 100
symbols:
gt: 0 # more than 0
lt: 50 # fewer than 50
expected:
errors:
empty: true # must be empty
results:
empty: false # must NOT be empty
expected:
grammars:
includes: [ruby, python] # must include ALL of these
excludes: [cobol] # must include NONE of these
contains: ruby # must include this single value
expected:
annotations:
first: # first element must match this shape
comment_text: "# @requirement:DL-URS-001"
last:
path: "lib/dark_lantern/engine.rb"
any: # at least one element must match
kind: class
name: Pipeline
Partial matching means only the specified fields are checked. Unspecified fields are ignored. This is critical — you don't need to match the entire object.
expected:
content:
matches: "def execute" # regex match against string value
expected:
context:
has_key: repo_path # hash must contain this key
keys: [repo, repo_path, grammars] # exact key set (sorted)
expected:
result:
not:
status: error # result must NOT match this shape
Matchers compose. You can nest exact values inside shape matchers:
expected:
annotations:
count: 3
any:
comment_text:
matches: "@requirement:DL-URS-010"
code_text:
matches: "class AnnotationQuery"
Scenarios live in spec/scenarios/ organized by boundary:
spec/scenarios/
├── resolve_repo/
│ ├── 01_known_repo.yml
│ ├── 02_unknown_repo.yml
│ └── 03_local_vs_remote.yml
├── file_serve/
│ ├── 01_full_file.yml
│ └── 02_line_range.yml
└── annotation_query/
├── 01_single_annotation.yml
└── 05_self_trace.yml
Numbering controls order. The game engine sorts by filename.
# Load all scenarios
scenarios = Wanderland::Scenario.load_all("spec/scenarios")
# Load by operation (boundary name)
scenarios = Wanderland::Scenario.load_by_operation("spec/scenarios", :resolve_repo)
# Run and verify
scenario = scenarios.first
scenario.run # calls Wanderland::Boundary.execute(operation, input)
failures = scenario.verify # returns array of failure strings (empty = pass)
scenario.passed? # true if no failures
# Game mode
result = scenario.check_answer("symbol_name", "execute")
# => { blank: "symbol_name", user: "execute", expected: "execute",
# actual: "execute", user_correct: true, engine_correct: true }
Scenarios dispatch to boundaries by name. The boundary must be registered before running:
# Class-based (auto-registers via the boundary declaration)
class ResolveRepo
include Wanderland::Boundary
boundary :resolve_repo
def call(input) ... end
end
# Proc-based
Wanderland::Boundary.register(:resolve_repo) { |input| ... }
In dark-lantern, boundaries auto-discover from the boundary_path config directory at boot. Drop a .rb file with boundary :name, it registers.
Each scenario should test one thing. Use exact values for critical fields, matchers for structure. The description should explain what boundary behavior is being verified and why it matters.
For game mode, blanks should ask about the interesting parts — the fields that require understanding, not just reading. The hint should teach, not give away the answer.
The scenario is both a test case and a teaching tool. Write it for both audiences.
ShapeMatcher reserves these key names as matchers: count, gte, lte, gt, lt, matches, contains, includes, excludes, first, last, any, keys, has_key, not, empty.
If your boundary output has a field named count, first, last, any, keys, empty, or not, ShapeMatcher will interpret it as a matcher, not a field assertion. The expected block count: 37 means "the parent has 37 elements", not "the field count equals 37".
Avoid naming output fields after matcher keywords. If you inherit a field name you can't change, assert on it through blanks (which uses dot-path dig, not ShapeMatcher) or restructure the expected block to avoid the collision."