Testing Guide
Chit provides test helpers in the github.com/zoobz-io/chit/testing package. This guide covers how to test processors, emitters, and chat interactions.
Test Helpers
Import the testing package:
import (
"github.com/zoobz-io/chit"
helpers "github.com/zoobz-io/chit/testing"
)
MockProcessor
A configurable mock that records calls and returns controlled responses.
func TestMyFeature(t *testing.T) {
mock := &helpers.MockProcessor{
ProcessFunc: func(ctx context.Context, input string, session *zyn.Session) (chit.Result, error) {
return &chit.Response{Content: "mocked: " + input}, nil
},
}
chat := chit.New(mock, &helpers.CollectingEmitter{})
_ = chat.Handle(context.Background(), "hello")
// Verify calls
if len(mock.Calls) != 1 {
t.Errorf("expected 1 call, got %d", len(mock.Calls))
}
if mock.Calls[0].Input != "hello" {
t.Errorf("expected input 'hello', got %q", mock.Calls[0].Input)
}
}
Fields:
ProcessFunc— custom behavior (optional, defaults to empty response)Calls— slice ofProcessCall{Input, Session}for verification
MockEmitter
A configurable mock for testing output behavior.
func TestEmitterErrors(t *testing.T) {
emitErr := errors.New("emit failed")
mock := &helpers.MockEmitter{
EmitFunc: func(ctx context.Context, msg chit.Message) error {
return emitErr
},
}
chat := chit.New(helpers.EchoProcessor(), mock)
err := chat.Handle(context.Background(), "test")
if !errors.Is(err, emitErr) {
t.Errorf("expected emit error, got %v", err)
}
}
Fields:
EmitFunc,PushFunc,CloseFunc— custom behavior (optional)Messages— recorded emitted messagesResources— recorded pushed resourcesClosed— whether Close was called
CollectingEmitter
A simple emitter that collects all output for assertions.
func TestChatOutput(t *testing.T) {
emitter := &helpers.CollectingEmitter{}
processor := helpers.EchoProcessor()
chat := chit.New(processor, emitter)
_ = chat.Handle(context.Background(), "hello")
// Check collected content
if emitter.Content() != "Echo: hello" {
t.Errorf("unexpected content: %q", emitter.Content())
}
// Or inspect individual messages
if len(emitter.Messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(emitter.Messages))
}
if emitter.Messages[0].Role != "assistant" {
t.Errorf("expected assistant role")
}
}
Methods:
Content()— returns all message content concatenated
EchoProcessor
A processor that echoes input, useful for testing chat flow.
processor := helpers.EchoProcessor()
// Returns Response{Content: "Echo: " + input}
YieldingProcessor
A processor that yields on first call and responds on continuation.
processor := helpers.YieldingProcessor("What's your name?", "Hello")
// First call yields
result, _ := processor.Process(ctx, "start", session)
yield := result.(*chit.Yield)
// yield.Prompt == "What's your name?"
// Continuation responds
resumed, _ := yield.Continuation(ctx, "Alice")
resp := resumed.(*chit.Response)
// resp.Content == "Hello: Alice"
Testing Patterns
Testing a Custom Processor
func TestMyProcessor(t *testing.T) {
processor := NewMyProcessor(/* dependencies */)
session := zyn.NewSession()
result, err := processor.Process(context.Background(), "test input", session)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
resp, ok := result.(*chit.Response)
if !ok {
t.Fatal("expected Response")
}
if resp.Content != "expected output" {
t.Errorf("unexpected content: %q", resp.Content)
}
}
Testing Multi-Turn Flows
func TestMultiTurnFlow(t *testing.T) {
emitter := &helpers.CollectingEmitter{}
processor := helpers.YieldingProcessor("Name?", "Hello")
chat := chit.New(processor, emitter)
ctx := context.Background()
// First turn yields
_ = chat.Handle(ctx, "start")
if !chat.HasContinuation() {
t.Fatal("expected continuation after yield")
}
// Second turn resumes
_ = chat.Handle(ctx, "Alice")
if chat.HasContinuation() {
t.Error("expected no continuation after response")
}
// Verify final output includes resumed response
if !strings.Contains(emitter.Content(), "Hello: Alice") {
t.Errorf("expected resumed response in output: %q", emitter.Content())
}
}
Testing Emitter Push
func TestResourcePush(t *testing.T) {
emitter := &helpers.CollectingEmitter{}
processor := chit.ProcessorFunc(func(ctx context.Context, input string, session *zyn.Session) (chit.Result, error) {
if e := chit.EmitterFromContext(ctx); e != nil {
e.Push(ctx, chit.Resource{
Type: "data",
URI: "test://resource",
})
}
return &chit.Response{Content: "done"}, nil
})
chat := chit.New(processor, emitter)
_ = chat.Handle(context.Background(), "test")
if len(emitter.Resources) != 1 {
t.Fatalf("expected 1 resource, got %d", len(emitter.Resources))
}
if emitter.Resources[0].URI != "test://resource" {
t.Errorf("unexpected URI: %q", emitter.Resources[0].URI)
}
}
Testing Error Handling
func TestProcessorError(t *testing.T) {
expectedErr := errors.New("processor failed")
processor := &helpers.MockProcessor{
ProcessFunc: func(ctx context.Context, input string, session *zyn.Session) (chit.Result, error) {
return nil, expectedErr
},
}
chat := chit.New(processor, &helpers.CollectingEmitter{})
err := chat.Handle(context.Background(), "test")
if !errors.Is(err, expectedErr) {
t.Errorf("expected processor error, got %v", err)
}
}
Testing with Signals
func TestSignalEmission(t *testing.T) {
var receivedChatID string
// Subscribe before creating chat
cancel := capitan.Subscribe(chit.ChatCreated, func(ctx context.Context, fields capitan.Fields) {
receivedChatID = chit.FieldChatID.Get(fields)
})
defer cancel()
chat := chit.New(helpers.EchoProcessor(), &helpers.CollectingEmitter{})
if receivedChatID != chat.ID() {
t.Errorf("expected chat ID %q, got %q", chat.ID(), receivedChatID)
}
}
Next Steps
- Troubleshooting Guide — common issues and solutions
- API Reference — function documentation
- Types Reference — type documentation