cassette¶
import "github.com/jbcom/radioactive-ralph/internal/session/cassette"
Package cassette provides a VCR-style record/replay layer for the session wrapper. Cassettes capture the stdin (user messages) and stdout (stream-json frames) of a real `claude -p` subprocess so tests can replay the same conversation deterministically without needing API credentials.
The design is subprocess-level rather than HTTP-level: claude’s actual API calls happen inside the subprocess and we never see the HTTP traffic. Cassettes therefore replay the observable I/O of claude itself, which is exactly the surface session.Session depends on.
Cassette usage flow:
Record once (requires ANTHROPIC_API_KEY or authenticated Claude Code install):
rec, _ := cassette.NewRecorder(cassettePath, realClaudeBin, args) // rec exposes the same file descriptors Spawn uses // drive the session normally, rec writes the JSON cassette
Replay (no auth required, runs in CI):
opts.ClaudeBin = cassette.ReplayerPath os.Setenv(“RALPH_CASSETTE_PATH”, cassettePath) // session.Spawn exec’s the replayer, which reads the cassette // and emits the recorded frames with recorded timing.
Cassettes are JSON so diffs in code review are meaningful.
Index¶
Constants¶
CurrentVersion is the schema version the recorder writes.
const CurrentVersion = 1
type Cassette¶
Cassette is the top-level on-disk format.
type Cassette struct {
// Version identifies the cassette schema. Bumped on
// incompatible changes.
Version int `json:"version"`
// RecordedAt is when the cassette was captured. Informational.
RecordedAt time.Time `json:"recorded_at"`
// ClaudeVersion is whatever `claude --version` reported when the
// cassette was recorded. Tests can warn (not fail) when the
// installed claude differs.
ClaudeVersion string `json:"claude_version,omitempty"`
// Args are the CLI args claude was invoked with at record time,
// minus --session-id (which the replayer must honor from the
// actual invocation).
Args []string `json:"args,omitempty"`
// Frames is the ordered stream of recorded events. Each frame is
// either an inbound frame from claude (stdin → stdout direction
// from the cassette's POV, which means stdout from the
// replayer's POV) or a user-input marker noting when stdin
// received a line from the client.
Frames []Frame `json:"frames"`
}
func Load¶
func Load(path string) (*Cassette, error)
Load reads a cassette from disk.
func (*Cassette) Save¶
func (c *Cassette) Save(path string) error
Save writes c to path as indented JSON for readable diffs.
type Frame¶
Frame is one entry in a cassette.
type Frame struct {
// Direction is "in" (stdin received from client) or "out" (stdout
// emitted to client). During replay, "in" frames act as
// checkpoints: the replayer waits for the client to send a
// matching user message before emitting subsequent "out" frames.
Direction string `json:"dir"`
// At is the offset from the session start when this frame was
// observed, in nanoseconds. Replayer enforces a minimum gap
// between consecutive "out" frames based on these offsets.
At time.Duration `json:"at_ns"`
// Line is the raw JSON line that was observed (no trailing \n).
// For "in" frames, this is the outbound stream-json object the
// client sent. For "out" frames, this is the inbound frame
// claude emitted.
Line json.RawMessage `json:"line"`
}
type Recorder¶
Recorder wraps a real claude subprocess and captures its I/O to a cassette. The public surface mirrors what session.Session needs: Stdin writer, Stdout reader, Start, Wait, Close.
Typical flow:
rec, err := NewRecorder(ctx, cassettePath, "claude", argv)
stdin := rec.Stdin()
stdout := rec.Stdout()
rec.Start() // spawns claude; I/O is passed through + taped
// ... drive session via stdin/stdout as usual ...
rec.Close() // flushes cassette to disk
type Recorder struct {
// contains filtered or unexported fields
}
func NewRecorder¶
func NewRecorder(ctx context.Context, cassettePath, bin string, args []string) (*Recorder, error)
NewRecorder returns an uninitialized Recorder. Callers must call Start before any I/O and Close after the session ends.
func (*Recorder) Close¶
func (r *Recorder) Close() error
Close stops the subprocess, flushes the cassette to disk, and returns any write error.
func (*Recorder) Start¶
func (r *Recorder) Start() error
Start spawns the claude subprocess. Returns any exec error.
func (*Recorder) Stdin¶
func (r *Recorder) Stdin() io.WriteCloser
Stdin returns the client-side writer. Anything written here is forwarded to claude and recorded to the cassette.
func (*Recorder) Stdout¶
func (r *Recorder) Stdout() io.ReadCloser
Stdout returns the client-side reader. Anything read here was emitted by claude (and also recorded).
func (*Recorder) Wait¶
func (r *Recorder) Wait() error
Wait waits for the subprocess to exit.
Generated by gomarkdoc