--- title: internal/session/cassette description: Go API reference for the cassette package. --- # cassette ```go 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: 1. 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 2. 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](<#constants>) - [type Cassette](<#Cassette>) - [func Load\(path string\) \(\*Cassette, error\)](<#Load>) - [func \(c \*Cassette\) Save\(path string\) error](<#Cassette.Save>) - [type Frame](<#Frame>) - [type Recorder](<#Recorder>) - [func NewRecorder\(ctx context.Context, cassettePath, bin string, args \[\]string\) \(\*Recorder, error\)](<#NewRecorder>) - [func \(r \*Recorder\) Close\(\) error](<#Recorder.Close>) - [func \(r \*Recorder\) Start\(\) error](<#Recorder.Start>) - [func \(r \*Recorder\) Stdin\(\) io.WriteCloser](<#Recorder.Stdin>) - [func \(r \*Recorder\) Stdout\(\) io.ReadCloser](<#Recorder.Stdout>) - [func \(r \*Recorder\) Wait\(\) error](<#Recorder.Wait>) ## Constants CurrentVersion is the schema version the recorder writes. ```go const CurrentVersion = 1 ``` ## type [Cassette]() Cassette is the top\-level on\-disk format. ```go 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]() ```go func Load(path string) (*Cassette, error) ``` Load reads a cassette from disk. ### func \(\*Cassette\) [Save]() ```go 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. ```go 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 ``` ```go type Recorder struct { // contains filtered or unexported fields } ``` ### func [NewRecorder]() ```go 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]() ```go func (r *Recorder) Close() error ``` Close stops the subprocess, flushes the cassette to disk, and returns any write error. ### func \(\*Recorder\) [Start]() ```go func (r *Recorder) Start() error ``` Start spawns the claude subprocess. Returns any exec error. ### func \(\*Recorder\) [Stdin]() ```go 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]() ```go 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]() ```go func (r *Recorder) Wait() error ``` Wait waits for the subprocess to exit. Generated by [gomarkdoc]()