---
title: Testing Extensions
description: Write unit tests for your Kit extensions using the test package.
---

# Testing Extensions

Kit provides a testing package (`github.com/mark3labs/kit/pkg/extensions/test`) that enables you to write unit tests for your extensions. Tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance, allowing you to verify behavior without running the full Kit TUI.

## Overview

Extension tests allow you to:

- Test event handlers without running the interactive TUI
- Verify tool/command registration
- Assert that context methods (Print, SetWidget, etc.) are called correctly
- Test blocking and non-blocking event handling
- Simulate user input and tool calls
- Verify widget, header, footer, and status bar updates

## Installation

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

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

## Basic Usage

### Testing an Extension File

Create a test file alongside your extension (e.g., `my-ext_test.go`):

```go
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 check results
    result, err := harness.Emit(extensions.ToolCallEvent{
        ToolName: "my_tool",
        Input:    `{"key": "value"}`,
    })
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    // Use assertion helpers
    test.AssertNotBlocked(t, result)
    test.AssertPrinted(t, harness, "expected output")
}
```

### Testing Inline Extension Code

For quick tests or edge cases, you can load extension source directly:

```go
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 Handler Registration

Verify your extension registers the expected handlers:

```go
func TestHandlers(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    test.AssertHasHandlers(t, harness, extensions.ToolCall)
    test.AssertHasHandlers(t, harness, extensions.SessionStart)
    test.AssertNoHandlers(t, harness, extensions.AgentEnd) // Verify no unexpected handlers
}
```

### Testing Tool Registration

```go
func TestTools(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Verify a specific tool is registered
    test.AssertToolRegistered(t, harness, "my_tool")
    
    // Or inspect all tools
    tools := harness.RegisteredTools()
    for _, tool := range tools {
        t.Logf("Tool: %s - %s", tool.Name, tool.Description)
    }
}
```

### Testing Commands

```go
func TestCommands(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    test.AssertCommandRegistered(t, harness, "mycommand")
}
```

### Testing Widgets

```go
func TestWidgets(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Trigger event that creates the widget
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    // Verify widget was set
    test.AssertWidgetSet(t, harness, "my-widget")
    test.AssertWidgetText(t, harness, "my-widget", "Expected Text")
    test.AssertWidgetTextContains(t, harness, "my-widget", "partial")
    
    // Check widget properties directly
    widget, ok := harness.Context().GetWidget("my-widget")
    if ok {
        t.Logf("Border color: %s", widget.Style.BorderColor)
    }
}
```

### Testing Input Handling

```go
func TestInput(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    result, _ := harness.Emit(extensions.InputEvent{
        Text:   "!mycommand",
        Source: "cli",
    })
    
    test.AssertInputHandled(t, result, "handled")
}
```

### Testing Headers and Footers

```go
func TestHeaderFooter(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    test.AssertHeaderSet(t, harness)
    test.AssertFooterSet(t, harness)
    
    // Inspect content
    header := harness.Context().GetHeader()
    if header != nil {
        t.Logf("Header text: %s", header.Content.Text)
    }
}
```

### Testing Status Bar

```go
func TestStatus(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 Print Output

```go
func TestOutput(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "test"})
    
    // Exact match
    test.AssertPrinted(t, harness, "exact output")
    
    // Partial match
    test.AssertPrintedContains(t, harness, "partial")
    
    // Styled output
    test.AssertPrintInfo(t, harness, "info message")
    test.AssertPrintError(t, harness, "error message")
}
```

### Testing with Prompts

Configure mock prompt results for testing interactive behavior:

```go
func TestWithPrompts(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Configure what prompts should return
    harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
        Value:     "option1",
        Index:     0,
        Cancelled: false,
    })
    
    harness.Context().SetPromptConfirmResult(extensions.PromptConfirmResult{
        Value:     true,
        Cancelled: false,
    })
    
    // Now when your extension calls ctx.PromptSelect(), it gets this result
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
}
```

### Testing Complete Session Flow

```go
func TestFullSession(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Simulate a complete session
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    _, _ = harness.Emit(extensions.BeforeAgentStartEvent{})
    _, _ = harness.Emit(extensions.AgentStartEvent{})
    
    // Multiple tool calls
    tools := []string{"Read", "Grep", "Bash"}
    for _, tool := range tools {
        _, _ = harness.Emit(extensions.ToolCallEvent{ToolName: tool})
        _, _ = harness.Emit(extensions.ToolResultEvent{ToolName: tool})
    }
    
    _, _ = harness.Emit(extensions.AgentEndEvent{})
    _, _ = harness.Emit(extensions.SessionShutdownEvent{})
    
    // Verify final state
    test.AssertWidgetTextContains(t, harness, "status", "Complete")
}
```

## Available Assertions

The test package provides these assertion helpers:

### Event Results

| Function | Description |
|----------|-------------|
| `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 was transformed |

### Context Interactions

| Function | Description |
|----------|-------------|
| `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

| Function | Description |
|----------|-------------|
| `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

| Function | Description |
|----------|-------------|
| `AssertMessageSent(t, harness, text)` | Verify SendMessage was called |
| `AssertCancelAndSend(t, harness, text)` | Verify CancelAndSend was called |

## Helper Functions

For custom assertions, extract result details:

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

ir := test.GetInputResult(result)
trr := test.GetToolResultResult(result)
```

## Advanced Usage

### Accessing the Mock Context

For custom verification:

```go
ctx := harness.Context()

// Get all recorded prints
prints := ctx.GetPrints()

// Check options
value := ctx.GetOption("my-option")

// Verify widget properties
widget, ok := ctx.GetWidget("my-widget")
if ok && widget.Style.BorderColor == "#ff0000" {
    t.Log("Widget has red border")
}

// Check status entries
status, ok := ctx.GetStatus("myext:status")
```

### Testing Multiple Extensions

Each harness is isolated:

```go
harness1 := test.New(t)
harness1.LoadFile("ext1.go")

harness2 := test.New(t)
harness2.LoadFile("ext2.go")

// Events to one don't affect the other
```

### Running Tests

Run all tests in your extension directory:

```bash
cd examples/extensions
go test -v
```

Run with race detector:

```bash
go test -race -v
```

Run a specific test:

```bash
go test -v -run TestMyExtension
```

## Best Practices

1. **Test one behavior per test** — Keep tests focused and readable
2. **Use inline source for edge cases** — `LoadString()` is great for testing specific scenarios
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
7. **Use descriptive test names** — `TestExtension_BlocksDangerousTools` is clearer than `Test1`

## Limitations

The test harness has these intentional limitations:

- **No TUI rendering** — Widgets are recorded but not rendered visually
- **Prompts return configured values** — Pre-configure prompt results in tests
- **Subagents don't spawn real processes** — `SpawnSubagent()` returns nil/empty results
- **LLM completions are mocked** — `Complete()` returns empty responses
- **Some context methods are no-ops** — `Exit()`, `SetActiveTools()`, etc. don't have side effects

These limitations focus testing on extension logic rather than the full Kit runtime.

## Complete Example

See `examples/extensions/tool-logger_test.go` for a complete example with 14 tests covering:

- Handler registration
- Tool call and result handling
- Session lifecycle events
- Input commands (`!time`, `!status`)
- Unknown command handling
- Concurrent operations (race condition check)
- Real file logging verification
