Files
kit/extensions/testing/index.html
T

460 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Testing Extensions | Kit</title>
<meta name="description" content="Write unit tests for your Kit extensions using the test package.">
<link rel="canonical" href="/extensions/testing">
<link rel="stylesheet" href="/assets/index-Di_r5hA0.css">
<script type="module" src="/assets/index-DfN-NIzu.js"></script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Testing Extensions","description":"Write unit tests for your Kit extensions using the test package.","url":"https://go-kit.dev/extensions/testing","isPartOf":{"@type":"WebSite","name":"Kit","url":"https://go-kit.dev"}}</script>
</head>
<body>
<div id="tome-root"></div>
<div data-pagefind-body style="display:none"><h1>Testing Extensions</h1>
# 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</div>
</body>
</html>