Testing
Sharpee uses transcript testing — text files that describe a sequence of player commands and expected outcomes. The transcript tester runs these against the game engine and verifies that output, events, and world state match expectations.
There are two kinds of transcript tests:
- Unit tests: Short, isolated tests for specific features or puzzles. Each gets a fresh game instance.
- Walkthroughs: Long, chained playthroughs where game state persists between files. Used to verify full game progression.
Quick Start
From your story project directory:
# Build your story with a test runner bundlenpx sharpee build --test
# Run all transcript testsnode my-story-test.js --test tests/transcripts/*.transcript
# Run walkthroughs as a chainnode my-story-test.js --test --chain walkthroughs/wt-*.transcript
# With verbose outputnode my-story-test.js --test --verbose walkthroughs/wt-*.transcript
# Stop on first failurenode my-story-test.js --test --stop-on-failure walkthroughs/wt-*.transcriptnpx sharpee build --test compiles your story and produces a {story}-test.js bundle — a single file that includes the Sharpee engine and your story, ready to run transcript tests. The test runner finds:
walkthroughs/wt-*.transcript— run as a chain (state persists between files)tests/transcripts/*.transcript— run individually (fresh game per file)
Project Layout
my-story/├── walkthroughs/│ ├── wt-01-first-quest.transcript # Walkthrough files (chained)│ ├── wt-02-second-quest.transcript│ └── ...├── tests/│ └── transcripts/│ ├── door-puzzle.transcript # Unit tests (isolated)│ ├── combat.transcript│ └── ...└── saves/ ├── wt-01.json # Auto-generated save checkpoints └── wt-02.jsonWriting Transcripts
File Structure
Every transcript starts with a YAML header, followed by ---, then commands and assertions:
title: Door Puzzle Teststory: my-storydescription: Tests that the locked door requires the brass key
---
# Comments start with #
> look[OK: contains "a locked door"]
> open door[OK: contains "locked"]
> unlock door with brass key[OK: contains "unlocked"]
> open door[OK: contains "opens"]Commands
Lines starting with > are player commands sent to the game:
> take lamp> go north> open chestSections
Lines starting with ## are section headers. They appear in test output to organize results but don’t affect execution:
## Setup> take sword[OK: contains "Taken"]
## Combat> attack troll with sword[OK: contains "troll"]Assertions
Assertions follow a command and verify the result. Every assertion is enclosed in [brackets].
Text Assertions
Check the text output of a command:
| Assertion | Meaning |
|---|---|
[OK: contains "text"] | Output includes the substring |
[OK: not contains "text"] | Output does NOT include the substring |
[OK: contains_any "a" "b" "c"] | Output includes at least one of the strings |
[OK: matches /pattern/flags] | Output matches a regex |
> look[OK: contains "Living Room"][OK: not contains "darkness"]
> inventory[OK: contains_any "sword" "knife" "dagger"]
> examine paintings[OK: matches /\d+ paintings?/i]Expecting Failure
Use [FAIL] to assert a check should NOT pass (inverts the logic):
> east[FAIL: contains "blocked"]Skip / TODO
> complex command[SKIP]
> unimplemented feature[TODO: not yet implemented]Event Assertions
Verify semantic events emitted by the engine during the turn:
| Assertion | Meaning |
|---|---|
[EVENT: true, type="if.event.taken"] | Event was emitted |
[EVENT: false, type="if.event.taken"] | Event was NOT emitted |
[EVENT: true, 1, type="if.event.opened"] | Event at position 1 (1-indexed) |
[EVENT: true, type="action.success" messageId="x"] | Event with specific data |
[EVENTS: 3] | Exactly 3 events were emitted |
> take lantern[EVENT: true, type="if.event.taken"]
> open chest[EVENT: true, 1, type="if.event.opened"][EVENTS: 2]State Assertions
Verify world model state after a command:
> take egg[STATE: true, egg.location = player]
> drop egg[STATE: true, egg.location != player]
> inventory[STATE: true, player.inventory contains lantern][STATE: false, player.inventory contains sword]Control Flow Directives
Transcripts support directives for handling variable outcomes, loops, and navigation.
GOAL / END GOAL
Organize transcripts into named objectives with preconditions and postconditions:
[GOAL: Get the torch][REQUIRES: inventory contains "rope"][ENSURES: inventory contains "torch"]
> tie rope to railing[OK: contains "rope"]
> down[OK: contains "Torch Room"]
> take torch[OK: contains "Taken"]
[END GOAL][REQUIRES: condition]— must be true before the goal starts (test fails if not)[ENSURES: condition]— must be true after the goal completes (test fails if not)
IF / END IF
Conditional execution:
[IF: location = "Troll Room"]> attack troll with sword[END IF]WHILE / END WHILE
Loop while a condition is true (max 100 iterations):
[WHILE: room contains "troll"]> attack troll with sword[END WHILE]DO / UNTIL
Execute at least once, then repeat until output matches:
[DO]> attack troll with sword[OK: contains_any "troll" "staggering"][UNTIL "slumps to the floor dead" OR "He dies" OR "You are dead"]Multiple UNTIL conditions use OR logic. Output accumulates across iterations.
RETRY
Retry a block on failure, restoring world state between attempts:
[RETRY: max=5][DO]> attack troll with sword[OK: contains_any "troll" "staggering"][UNTIL "slumps to the floor dead" OR "He dies" OR "You are dead"][ENSURES: not entity "troll" alive][END RETRY]This is essential for handling randomness (combat outcomes, NPC behavior). The tester saves world state before the block and restores it on failure before retrying.
NAVIGATE TO
Auto-pathfind to a room by name:
[NAVIGATE TO: "Torch Room"]Uses BFS pathfinding on the world model. Useful for skipping tedious navigation in long tests.
Save and Restore
Transcripts can create and load checkpoints:
# At the end of a walkthrough$save wt-01
# At the start of the next walkthrough$restore wt-01Saves are written to saves/{name}.json as serialized world state. This is how walkthrough chains work — each walkthrough saves at the end, and the next one restores from that checkpoint.
Test Commands
Transcripts can use $ prefixed commands for testing utilities (requires the TestingExtension):
$teleport kitchen # Move player to a room$take egg # Put item in inventory$kill troll # Kill an entity$immortal # Player can't die$mortal # Restore mortality$state entity.prop = val # Set entity state$describe entity # Dump entity infoCondition Expressions
Used in REQUIRES, ENSURES, IF, and WHILE directives:
| Expression | Meaning |
|---|---|
location = "Room Name" | Player is in that room |
room contains "entity" | Entity is in the current room |
not room contains "entity" | Entity is NOT in the current room |
inventory contains "item" | Player is carrying the item |
not inventory contains "item" | Player is NOT carrying the item |
entity "X" in "Room" | Entity X is in the specified room |
entity "X" alive | Entity X is alive |
not entity "X" alive | Entity X is dead |
Unit Tests vs Walkthroughs
Unit Tests
Location: tests/transcripts/*.transcript
- Test one feature, puzzle, or mechanic in isolation
- Get a fresh game instance each run
- Use
$teleportand$taketo set up the scenario quickly - Keep them short and focused
Example:
title: Basket Elevatorstory: my-storydescription: Tests basket elevator lowering/raising
---
## Setup$teleport Shaft Room$take brass lantern
> turn on lantern[OK: contains "switches on"]
## Test lowering> lower basket[OK: contains "lower"]
## Already at bottom> lower basket[OK: contains "already"]
## Raise it back> raise basket[OK: contains "raise"]Walkthroughs
Location: walkthroughs/wt-NN-description.transcript
- Test full game progression from start to finish
- State persists between files via
$save/$restore - Run as a chain with
node {story}-test.js --test --chain walkthroughs/wt-*.transcript - Number them sequentially:
wt-01,wt-02, etc. - Each walkthrough saves at the end so the next one can continue
Example pattern:
title: Get Torch Earlystory: my-storydescription: Get the torch ASAP to save lantern battery
---
[GOAL: Collect essential items from house][ENSURES: inventory contains "rope"][ENSURES: inventory contains "lantern"][ENSURES: inventory contains "sword"]
> look[OK: contains "West of House"]
> north...
[END GOAL]
# Save for next walkthrough$save wt-01title: Bank Puzzlestory: my-story
---
$restore wt-01
[GOAL: Solve the bank vault]...[END GOAL]
$save wt-02Test Results
Each command produces one of:
| Result | Meaning |
|---|---|
| PASS | All assertions passed |
| FAIL | At least one assertion failed |
| EXPECTED FAIL | Marked [FAIL] and failed as intended |
| SKIP | Marked [SKIP] or [TODO] |
Summary output:
360 tests: 349 pass, 0 fail, 0 expected-fail, 11 skip (1623ms)CLI Reference
npx sharpee build
| Flag | Description |
|---|---|
--test | Produce a {story}-test.js test runner bundle |
--no-minify | Skip browser client minification |
--no-sourcemap | Skip source map generation |
node {story}-test.js
| Flag | Description |
|---|---|
--test <files> | Run transcript test files |
--chain | Chain transcripts (game state persists between files) |
--verbose, -v | Show detailed test output |
--stop-on-failure | Stop on first test failure |