test

package
v0.48.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 10, 2026 License: MIT Imports: 9 Imported by: 0

README

Testing Kit Extensions

The github.com/mark3labs/kit/pkg/extensions/test package provides utilities for testing Kit extensions using standard Go testing patterns.

Overview

Extension tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance. This allows you to:

  • Test event handlers without running the full Kit TUI
  • Verify that your extension registers tools/commands correctly
  • Assert that context methods (Print, SetWidget, etc.) are called as expected
  • Test blocking and non-blocking event handling

Installation

The test package is part of the Kit codebase. Import it in your extension tests:

import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
    "github.com/mark3labs/kit/internal/extensions"
)

Basic Usage

Testing an Extension File
package main

import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
    "github.com/mark3labs/kit/internal/extensions"
)

func TestMyExtension(t *testing.T) {
    // Create a test harness
    harness := test.New(t)
    
    // Load your extension
    harness.LoadFile("my-ext.go")
    
    // Emit events and verify behavior
    _, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    // Verify the extension printed something
    test.AssertPrinted(t, harness, "session started")
}
Testing Inline Extension Code

For quick tests, you can load extension source directly:

func TestToolBlocking(t *testing.T) {
    src := `package main

import "kit/ext"

func Init(api ext.API) {
    api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
        if tc.ToolName == "dangerous" {
            return &ext.ToolCallResult{Block: true, Reason: "not allowed"}
        }
        return nil
    })
}
`
    harness := test.New(t)
    harness.LoadString(src, "test-ext.go")
    
    // Test the tool is blocked
    result, _ := harness.Emit(extensions.ToolCallEvent{
        ToolName: "dangerous",
        Input:    "{}",
    })
    
    test.AssertBlocked(t, result, "not allowed")
}

Common Testing Patterns

Testing Tool Registration
func TestToolRegistration(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Verify the tool was registered
    test.AssertToolRegistered(t, harness, "my_tool")
    
    // Or inspect tools directly
    tools := harness.RegisteredTools()
    for _, tool := range tools {
        if tool.Name == "my_tool" {
            t.Logf("Tool description: %s", tool.Description)
        }
    }
}
Testing Command Registration
func TestCommandRegistration(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    test.AssertCommandRegistered(t, harness, "mycommand")
}
Testing Widgets
func TestWidgetBehavior(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Trigger the event that creates the widget
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    // Verify the widget was set
    test.AssertWidgetSet(t, harness, "my-widget")
    
    // Verify specific widget content
    test.AssertWidgetText(t, harness, "my-widget", "Expected Text")
    
    // Or verify partial content
    test.AssertWidgetTextContains(t, harness, "my-widget", "partial")
}
Testing Input Handling
func TestInputHandling(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Test that the extension handles certain input
    result, _ := harness.Emit(extensions.InputEvent{
        Text:   "secret password",
        Source: "cli",
    })
    
    test.AssertInputHandled(t, result, "handled")
}
Testing Print Functions
func TestPrintOutput(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.ToolCallEvent{
        ToolName: "test",
        Input:    "{}",
    })
    
    // Assert exact match
    test.AssertPrinted(t, harness, "exact output")
    
    // Or partial match
    test.AssertPrintedContains(t, harness, "partial")
    
    // Assert info/error messages
    test.AssertPrintInfo(t, harness, "info message")
    test.AssertPrintError(t, harness, "error message")
}
Testing Status Bar
func TestStatusBar(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.AgentEndEvent{})
    
    test.AssertStatusSet(t, harness, "myext:status")
    test.AssertStatusText(t, harness, "myext:status", "Ready")
}
Testing Prompt Results

Configure the mock context to return specific prompt results:

func TestWithPrompts(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Configure prompt results before emitting events
    harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
        Value: "option1",
        Index: 0,
        Cancelled: false,
    })
    
    // Now when your extension calls ctx.PromptSelect(), it will get this result
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
}

Available Assertions

The test package provides these assertion helpers:

Event Results:

  • AssertNotBlocked(t, result) - Verify tool was not blocked
  • AssertBlocked(t, result, reason) - Verify tool was blocked with reason
  • AssertInputHandled(t, result, action) - Verify input was handled
  • AssertInputTransformed(t, result, text) - Verify input transformation

Context Interactions:

  • AssertPrinted(t, harness, text) - Verify exact print output
  • AssertPrintedContains(t, harness, substring) - Verify partial print output
  • AssertPrintInfo(t, harness, text) - Verify PrintInfo was called
  • AssertPrintError(t, harness, text) - Verify PrintError was called
  • AssertWidgetSet(t, harness, id) - Verify widget was set
  • AssertWidgetNotSet(t, harness, id) - Verify widget was not set
  • AssertWidgetText(t, harness, id, text) - Verify widget content
  • AssertWidgetTextContains(t, harness, id, substring) - Verify widget contains text
  • AssertHeaderSet(t, harness) - Verify header was set
  • AssertFooterSet(t, harness) - Verify footer was set
  • AssertStatusSet(t, harness, key) - Verify status was set
  • AssertStatusText(t, harness, key, text) - Verify status text

Registration:

  • AssertToolRegistered(t, harness, name) - Verify tool registration
  • AssertCommandRegistered(t, harness, name) - Verify command registration
  • AssertHasHandlers(t, harness, eventType) - Verify handlers exist
  • AssertNoHandlers(t, harness, eventType) - Verify no handlers

Messaging:

  • AssertMessageSent(t, harness, text) - Verify SendMessage was called
  • AssertCancelAndSend(t, harness, text) - Verify CancelAndSend was called

Advanced Usage

Accessing the Mock Context

For custom assertions, access the mock context directly:

func TestCustomAssertion(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    // Get all recorded prints
    prints := harness.Context().GetPrints()
    
    // Check widget directly
    widget, ok := harness.Context().GetWidget("my-widget")
    if ok && widget.Style.BorderColor == "#ff0000" {
        t.Log("Widget has red border")
    }
    
    // Check options
    optionValue := harness.Context().GetOption("my-option")
}
Testing Multiple Extensions

Each harness is isolated:

func TestExtensionIsolation(t *testing.T) {
    // These run in completely separate interpreters
    harness1 := test.New(t)
    harness1.LoadFile("ext1.go")
    
    harness2 := test.New(t)
    harness2.LoadFile("ext2.go")
    
    // Events to one don't affect the other
}
Direct Result Extraction

When you need to inspect result details:

result, _ := harness.Emit(extensions.ToolCallEvent{...})
tcr := test.GetToolCallResult(result)
if tcr != nil {
    t.Logf("Block: %v, Reason: %s", tcr.Block, tcr.Reason)
}

Best Practices

  1. Test one behavior per test - Keep tests focused and readable
  2. Use inline source for simple tests - LoadString is great for isolated tests
  3. Use LoadFile for integration tests - Tests the actual extension file
  4. Assert on context calls - Verify your extension interacts with the context correctly
  5. Test both positive and negative cases - Verify tools are blocked AND allowed appropriately
  6. Test all event handlers - Make sure all registered handlers work correctly

Limitations

The test harness has these limitations:

  1. No TUI rendering - Widgets are recorded but not rendered visually
  2. Prompts return configured values - You must pre-configure prompt results in tests
  3. Subagents don't spawn real processes - SpawnSubagent returns nil/empty results
  4. LLM completions are mocked - Complete returns empty responses
  5. Some context methods are no-ops - Exit, SetActiveTools, etc. don't have side effects

These limitations are intentional - the test harness focuses on testing extension logic, not the full Kit runtime.

Example: Complete Extension Test

Here's a complete example testing a realistic extension:

package main

import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
    "github.com/mark3labs/kit/internal/extensions"
)

// Test that the extension properly blocks dangerous tools
func TestSafetyExtension_BlocksDangerousTools(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("safety-ext.go")
    
    // Verify it handles tool calls
    test.AssertHasHandlers(t, harness, extensions.ToolCall)
    
    // Test allowed tool
    result, _ := harness.Emit(extensions.ToolCallEvent{ToolName: "read", Input: "{}"})
    test.AssertNotBlocked(t, result)
    
    // Test blocked tool
    result, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "rm", Input: "{}"})
    test.AssertBlocked(t, result, "safety block")
    test.AssertPrintError(t, harness, "Tool rm is blocked")
}

// Test that the extension shows status on agent completion
func TestSafetyExtension_ShowsStatus(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("safety-ext.go")
    
    _, _ = harness.Emit(extensions.AgentEndEvent{})
    
    test.AssertWidgetSet(t, harness, "safety-widget")
    test.AssertWidgetTextContains(t, harness, "safety-widget", "Safe")
}

Documentation

Overview

Package test provides utilities for testing Kit extensions.

This package allows extension authors to write standard Go tests that load and exercise their extensions in a controlled environment. Extensions are loaded into a Yaegi interpreter with all Kit API symbols available.

Basic usage:

package main

import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
)

func TestMyExtension(t *testing.T) {
    // Create a test harness
    harness := test.New(t)

    // Load your extension file
    ext := harness.LoadFile("my-ext.go")

    // Emit events and check results
    result := harness.Emit(test.ToolCallEvent{
        ToolName: "my_tool",
        Input:    `{"key": "value"}`,
    })

    // Use assertion helpers
    test.AssertNotBlocked(t, result)
    test.AssertPrinted(t, harness, "expected output")
}

The harness provides a mock Context that records all interactions, allowing you to verify that your extension called SetWidget, Print, etc.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertBlocked

func AssertBlocked(t *testing.T, result extensions.Result, expectedReason string)

AssertBlocked fails the test if the tool call result does not indicate the tool was blocked.

func AssertCancelAndSend

func AssertCancelAndSend(t *testing.T, harness *Harness, expected string)

AssertCancelAndSend fails the test if the expected text was not sent via CancelAndSend.

func AssertCommandRegistered

func AssertCommandRegistered(t *testing.T, harness *Harness, cmdName string)

AssertCommandRegistered fails the test if the command with the given name was not registered.

func AssertFooterSet

func AssertFooterSet(t *testing.T, harness *Harness)

AssertFooterSet fails the test if no footer was set.

func AssertHasHandlers

func AssertHasHandlers(t *testing.T, harness *Harness, eventType extensions.EventType)

AssertHasHandlers fails the test if no handlers are registered for the given event type.

func AssertHeaderSet

func AssertHeaderSet(t *testing.T, harness *Harness)

AssertHeaderSet fails the test if no header was set.

func AssertInputHandled

func AssertInputHandled(t *testing.T, result extensions.Result, expectedAction string)

AssertInputHandled fails the test if the input result does not indicate the input was handled.

func AssertInputTransformed

func AssertInputTransformed(t *testing.T, result extensions.Result, expectedText string)

AssertInputTransformed fails the test if the input was not transformed to the expected text.

func AssertMessageSent

func AssertMessageSent(t *testing.T, harness *Harness, expected string)

AssertMessageSent fails the test if the expected message was not sent.

func AssertNoHandlers

func AssertNoHandlers(t *testing.T, harness *Harness, eventType extensions.EventType)

AssertNoHandlers fails the test if any handlers are registered for the given event type.

func AssertNotBlocked

func AssertNotBlocked(t *testing.T, result extensions.Result)

AssertNotBlocked fails the test if the tool call result indicates the tool was blocked.

func AssertPrintError

func AssertPrintError(t *testing.T, harness *Harness, expected string)

AssertPrintError fails the test if the expected error message was not printed.

func AssertPrintInfo

func AssertPrintInfo(t *testing.T, harness *Harness, expected string)

AssertPrintInfo fails the test if the expected info message was not printed.

func AssertPrinted

func AssertPrinted(t *testing.T, harness *Harness, expected string)

AssertPrinted fails the test if the expected text was not printed.

func AssertPrintedContains

func AssertPrintedContains(t *testing.T, harness *Harness, substring string)

AssertPrintedContains fails the test if no printed text contains the expected substring.

func AssertStatusSet

func AssertStatusSet(t *testing.T, harness *Harness, key string)

AssertStatusSet fails the test if the status with the given key was not set.

func AssertStatusText

func AssertStatusText(t *testing.T, harness *Harness, key string, expected string)

AssertStatusText fails the test if the status with the given key does not have the expected text.

func AssertToolRegistered

func AssertToolRegistered(t *testing.T, harness *Harness, toolName string)

AssertToolRegistered fails the test if the tool with the given name was not registered.

func AssertWidgetNotSet

func AssertWidgetNotSet(t *testing.T, harness *Harness, id string)

AssertWidgetNotSet fails the test if the widget with the given ID was set.

func AssertWidgetSet

func AssertWidgetSet(t *testing.T, harness *Harness, id string)

AssertWidgetSet fails the test if the widget with the given ID was not set.

func AssertWidgetText

func AssertWidgetText(t *testing.T, harness *Harness, id string, expected string)

AssertWidgetText fails the test if the widget with the given ID does not have the expected text.

func AssertWidgetTextContains

func AssertWidgetTextContains(t *testing.T, harness *Harness, id string, substring string)

AssertWidgetTextContains fails the test if the widget text does not contain the expected substring.

func GetInputResult

func GetInputResult(result extensions.Result) *extensions.InputResult

GetInputResult extracts an InputResult from a Result, or nil if not applicable.

func GetToolCallResult

func GetToolCallResult(result extensions.Result) *extensions.ToolCallResult

GetToolCallResult extracts a ToolCallResult from a Result, or nil if not applicable.

func GetToolResultResult

func GetToolResultResult(result extensions.Result) *extensions.ToolResultResult

GetToolResultResult extracts a ToolResultResult from a Result, or nil if not applicable.

Types

type Harness

type Harness struct {
	// contains filtered or unexported fields
}

Harness provides a testing environment for Kit extensions. It loads extensions into an isolated Yaegi interpreter and provides methods to emit events and verify extension behavior.

func New

func New(t *testing.T) *Harness

New creates a new test harness for the given test. The harness must be used within a single test function.

func (*Harness) Context

func (h *Harness) Context() *MockContext

Context returns the mock context for inspection. Use this to verify Print calls, widget settings, etc.

func (*Harness) Emit

func (h *Harness) Emit(event extensions.Event) (extensions.Result, error)

Emit sends an event to the loaded extension(s) and returns the result. Events are dispatched in order and blocking results stop propagation.

func (*Harness) EmitJSON

func (h *Harness) EmitJSON(toolName string, input string) (*extensions.ToolCallResult, error)

EmitJSON is a convenience method for emitting a ToolCallEvent with JSON input.

func (*Harness) HasHandlers

func (h *Harness) HasHandlers(eventType extensions.EventType) bool

HasHandlers reports whether any handlers are registered for the given event type.

func (*Harness) LoadFile

func (h *Harness) LoadFile(path string) *extensions.LoadedExtension

LoadFile loads an extension from a file path. The extension is evaluated in a fresh Yaegi interpreter with all Kit API symbols available. The Init function is called automatically.

Returns the loaded extension or fails the test on error.

func (*Harness) LoadString

func (h *Harness) LoadString(src string, path string) *extensions.LoadedExtension

LoadString loads an extension from a source string. Useful for inline extension tests. The path is used for error reporting.

func (*Harness) RegisteredCommands

func (h *Harness) RegisteredCommands() []extensions.CommandDef

RegisteredCommands returns all commands registered by the extension.

func (*Harness) RegisteredTools

func (h *Harness) RegisteredTools() []extensions.ToolDef

RegisteredTools returns all tools registered by the extension.

func (*Harness) Runner

func (h *Harness) Runner() *extensions.Runner

Runner returns the underlying runner for advanced use cases.

type MockContext

type MockContext struct {

	// Recorded calls
	Prints      []string
	PrintInfos  []string
	PrintErrors []string
	PrintBlocks []extensions.PrintBlockOpts
	Messages    []string
	CancelSends []string

	// Widget state
	Widgets       map[string]extensions.WidgetConfig
	RemovedIDs    []string
	Header        *extensions.HeaderFooterConfig
	Footer        *extensions.HeaderFooterConfig
	HeaderRemoved bool
	FooterRemoved bool

	// Context properties
	SessionID   string
	CWD         string
	Model       string
	Interactive bool

	// UI visibility
	UIVisibility *extensions.UIVisibility

	// Status entries
	StatusEntries map[string]extensions.StatusBarEntry
	RemovedStatus []string

	// Editor
	EditorConfig *extensions.EditorConfig
	EditorReset  bool
	EditorTexts  []string

	// Options
	Options map[string]string

	// Prompt results (configurable for testing)
	PromptSelectResult      extensions.PromptSelectResult
	PromptConfirmResult     extensions.PromptConfirmResult
	PromptInputResult       extensions.PromptInputResult
	PromptMultiSelectResult extensions.PromptMultiSelectResult

	// Overlay
	Overlays []extensions.OverlayConfig
	// contains filtered or unexported fields
}

MockContext records all interactions with the extension context. It provides a Context object that captures Print calls, widget settings, and other context operations for verification in tests.

func NewMockContext

func NewMockContext() *MockContext

NewMockContext creates a new mock context with default values.

func (*MockContext) GetFooter

func (m *MockContext) GetFooter() *extensions.HeaderFooterConfig

GetFooter returns the recorded footer configuration.

func (*MockContext) GetHeader

func (m *MockContext) GetHeader() *extensions.HeaderFooterConfig

GetHeader returns the recorded header configuration.

func (*MockContext) GetPrintErrors

func (m *MockContext) GetPrintErrors() []string

GetPrintErrors returns all recorded PrintError calls.

func (*MockContext) GetPrintInfos

func (m *MockContext) GetPrintInfos() []string

GetPrintInfos returns all recorded PrintInfo calls.

func (*MockContext) GetPrints

func (m *MockContext) GetPrints() []string

GetPrints returns all recorded Print calls.

func (*MockContext) GetStatus

func (m *MockContext) GetStatus(key string) (extensions.StatusBarEntry, bool)

GetStatus returns a recorded status entry by key.

func (*MockContext) GetWidget

func (m *MockContext) GetWidget(id string) (extensions.WidgetConfig, bool)

GetWidget returns a recorded widget by ID.

func (*MockContext) HasWidget

func (m *MockContext) HasWidget(id string) bool

HasWidget reports whether a widget with the given ID was set.

func (*MockContext) SetPromptConfirmResult

func (m *MockContext) SetPromptConfirmResult(result extensions.PromptConfirmResult)

SetPromptConfirmResult configures the result returned by PromptConfirm.

func (*MockContext) SetPromptInputResult

func (m *MockContext) SetPromptInputResult(result extensions.PromptInputResult)

SetPromptInputResult configures the result returned by PromptInput.

func (*MockContext) SetPromptMultiSelectResult

func (m *MockContext) SetPromptMultiSelectResult(result extensions.PromptMultiSelectResult)

SetPromptMultiSelectResult configures the result returned by PromptMultiSelect.

func (*MockContext) SetPromptSelectResult

func (m *MockContext) SetPromptSelectResult(result extensions.PromptSelectResult)

SetPromptSelectResult configures the result returned by PromptSelect.

func (*MockContext) ToContext

func (m *MockContext) ToContext() extensions.Context

ToContext returns a extensions.Context wired to record all interactions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL