zoobzio January 14, 2025 Edit this page

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 of ProcessCall{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 messages
  • Resources — recorded pushed resources
  • Closed — 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