← All libraries

CandyVcr

📼 CandyVcr

Record + replay candy-core sessions

port of x/vcr cassettes jsonl + yaml replay cell-grid assert cli

Tee every Msg + every byte a candy-core Program emits into a cassette file, then drive a fresh Program through the same cassette and assert the output matches — byte-strict via ByteAssertion or cell-grid-strict via ScreenAssertion (powered by CandyVt).

Install

composer require sugarcraft/candy-vcr

Record

use SugarCraft\Core\Program;
use SugarCraft\Vcr\Recorder;

(new Program($model))
    ->withRecorder(Recorder::open('/tmp/session.cas'))
    ->run();
// cassette is closed automatically when the loop ends.

Replay

use SugarCraft\Vcr\Player;
use SugarCraft\Vcr\Assert\ScreenAssertion;

$player = Player::open('/tmp/session.cas');
$result = $player->play(
    programFactory: fn ($input, $output, $loop) => new Program(
        new MyModel(),
        new ProgramOptions(input: $input, output: $output, loop: $loop, …),
    ),
    assertion: new ScreenAssertion(80, 24),
);
if (!$result->ok) {
    echo $result->diffSummary();
    exit(1);
}

Cassette format

Cassettes are streaming JSONL — each line is a self-contained event with t (timestamp in seconds) and k (event kind). See the full schema reference for all event types and the header structure.

CLI

vendor/bin/candy-vcr inspect session.cas               # list events
vendor/bin/candy-vcr replay  session.cas --speed=realtime  # stream to stdout
vendor/bin/candy-vcr diff    a.cas b.cas               # structural diff
vendor/bin/candy-vcr record  --output session.cas -- bash -c 'echo hi'  # record a PTY session

record subcommand

The record subcommand spawns a PTY session and captures all I/O to a cassette file:

vendor/bin/candy-vcr record --output session.cas --cols 132 --rows 40 -- bash -l
vendor/bin/candy-vcr record --no-ctty -- /bin/echo 'hello'   # non-interactive, no Ctrl+C wiring
vendor/bin/candy-vcr record --shell                          # spawn $SHELL -l
vendor/bin/candy-vcr record --env -- bash -c 'echo hi'         # capture filtered host env
vendor/bin/candy-vcr record --env-regex='/(SECRET|TOKEN)/i' -- bash -c 'echo hi'  # custom filter
vendor/bin/candy-vcr record --env-allow-secrets -- bash -c 'echo $API_KEY'  # ⚠️ DANGEROUS — no filtering

⚠️ --env-allow-secrets disables all secret-key filtering. The cassette will contain credentials verbatim — only use in fully isolated, trusted environments and never share the resulting cassette.

What's in the box

Cassette formatJSONL primary (line-streamable, diff-friendly), YAML secondary (hand-written test fixtures). Both round-trip through the same Cassette value object.
Recorder hookProgram::withRecorder() tees input bytes, output bytes (renderer + RawMsg + PrintMsg + cursor / title / mode), resize, and quit events.
Msg serializers14 builtin candy-core Msg types (KeyMsg, MouseMsg×4, WindowSizeMsg, FocusMsg, BlurMsg, PasteMsg×3, BackgroundColorMsg, ForegroundColorMsg, CursorPositionMsg) + JsonSerializable catch-all + extensible Registry.
PlayerDrives a fresh Program through a cassette. INSTANT mode for fast tests, REALTIME for visual demos. Supports both raw input bytes and Msg envelope payloads.
ByteAssertionStrict byte-equality with hex+printable diff window starting at the first divergence offset.
ScreenAssertionCell-grid equality via CandyVt. Tolerates ANSI-level reordering (redundant SGR, equivalent cursor moves, partial vs full repaints) — the recommended choice for round-trip tests.

Use it for

Source & demos

Try the quickstart →

API

ClassMethodDescription
Recorderopen(path)Open cassette for recording
Recorderclose()Close and finalize cassette
Playeropen(path)Open cassette for replay
Playerplay(factory, assertion)Replay session with assertion
ByteAssertionnew()Strict byte-equality assertion
ScreenAssertionnew(cols, rows)Cell-grid equality assertion
Cassetteevents()Iterate recorded events