mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
4ae03aab7c
Move the extension testing package from internal/extensions/test to pkg/extensions/test to make it publicly importable by external extension authors. Changes: - Moved test package files to pkg/extensions/test/ - Updated all imports from internal/ to pkg/ path: - README.md - examples/extensions/tool-logger_test.go - examples/extensions/extension_test_template.go - skills/kit-extensions/SKILL.md - www/pages/extensions/testing.md - pkg/extensions/test/README.md - pkg/extensions/test/harness.go The test package is now available for external import as: github.com/mark3labs/kit/pkg/extensions/test All tests pass with race detector.
359 lines
9.2 KiB
Go
359 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mark3labs/kit/internal/extensions"
|
|
"github.com/mark3labs/kit/pkg/extensions/test"
|
|
)
|
|
|
|
// Test that the tool-logger extension loads and registers handlers
|
|
func TestToolLogger_Loads(t *testing.T) {
|
|
harness := test.New(t)
|
|
ext := harness.LoadFile("tool-logger.go")
|
|
|
|
if ext == nil {
|
|
t.Fatal("extension should not be nil")
|
|
}
|
|
|
|
// Verify all expected handlers are registered
|
|
test.AssertHasHandlers(t, harness, extensions.ToolCall)
|
|
test.AssertHasHandlers(t, harness, extensions.ToolResult)
|
|
test.AssertHasHandlers(t, harness, extensions.SessionStart)
|
|
test.AssertHasHandlers(t, harness, extensions.SessionShutdown)
|
|
test.AssertHasHandlers(t, harness, extensions.Input)
|
|
}
|
|
|
|
// Test that tool calls are logged (handlers run without errors)
|
|
func TestToolLogger_ToolCall(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
// Emit a tool call event
|
|
result, err := harness.Emit(extensions.ToolCallEvent{
|
|
ToolName: "Read",
|
|
ToolCallID: "call-123",
|
|
Input: `{"file": "test.txt"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Tool logger should not block any tools
|
|
test.AssertNotBlocked(t, result)
|
|
}
|
|
|
|
// Test that tool results are processed
|
|
func TestToolLogger_ToolResult(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
content := "Hello, World!"
|
|
result, err := harness.Emit(extensions.ToolResultEvent{
|
|
ToolName: "Read",
|
|
Content: content,
|
|
IsError: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Tool logger should not modify results
|
|
if result != nil {
|
|
t.Error("expected nil result (no modification)")
|
|
}
|
|
}
|
|
|
|
// Test that error tool results are handled
|
|
func TestToolLogger_ToolResultError(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
result, err := harness.Emit(extensions.ToolResultEvent{
|
|
ToolName: "Bash",
|
|
Content: "command not found",
|
|
IsError: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result != nil {
|
|
t.Error("expected nil result (no modification)")
|
|
}
|
|
}
|
|
|
|
// Test session start handler
|
|
func TestToolLogger_SessionStart(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
_, err := harness.Emit(extensions.SessionStartEvent{
|
|
SessionID: "test-session-123",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Handler should run without errors (logs to file)
|
|
// Since file logging happens outside our mock, we just verify no errors
|
|
}
|
|
|
|
// Test session shutdown handler
|
|
func TestToolLogger_SessionShutdown(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
_, err := harness.Emit(extensions.SessionShutdownEvent{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test the !time command
|
|
func TestToolLogger_TimeCommand(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
result, err := harness.Emit(extensions.InputEvent{
|
|
Text: "!time",
|
|
Source: "cli",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
test.AssertInputHandled(t, result, "handled")
|
|
|
|
// Verify PrintInfo was called with a time message
|
|
infos := harness.Context().GetPrintInfos()
|
|
found := false
|
|
for _, info := range infos {
|
|
if strings.Contains(info, "Current time:") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected PrintInfo with 'Current time:', got: %v", infos)
|
|
}
|
|
}
|
|
|
|
// Test the !status command
|
|
func TestToolLogger_StatusCommand(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
result, err := harness.Emit(extensions.InputEvent{
|
|
Text: "!status",
|
|
Source: "cli",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
test.AssertInputHandled(t, result, "handled")
|
|
|
|
// Verify PrintBlock was called
|
|
blocks := harness.Context().PrintBlocks
|
|
if len(blocks) != 1 {
|
|
t.Fatalf("expected 1 PrintBlock call, got %d", len(blocks))
|
|
}
|
|
|
|
block := blocks[0]
|
|
if block.Subtitle != "tool-logger extension" {
|
|
t.Errorf("expected subtitle 'tool-logger extension', got %q", block.Subtitle)
|
|
}
|
|
if block.BorderColor != "#a6e3a1" {
|
|
t.Errorf("expected border color '#a6e3a1', got %q", block.BorderColor)
|
|
}
|
|
if !strings.Contains(block.Text, "Session active") {
|
|
t.Errorf("expected text to contain 'Session active', got %q", block.Text)
|
|
}
|
|
}
|
|
|
|
// Test that unknown commands are not handled
|
|
func TestToolLogger_UnknownCommand(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
result, err := harness.Emit(extensions.InputEvent{
|
|
Text: "!unknown",
|
|
Source: "cli",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("expected nil result for unknown command, got %v", result)
|
|
}
|
|
|
|
// Verify no info/block prints for unknown commands
|
|
if len(harness.Context().GetPrintInfos()) != 0 {
|
|
t.Error("expected no PrintInfo calls for unknown command")
|
|
}
|
|
if len(harness.Context().PrintBlocks) != 0 {
|
|
t.Error("expected no PrintBlock calls for unknown command")
|
|
}
|
|
}
|
|
|
|
// Test regular text input (not a command)
|
|
func TestToolLogger_RegularInput(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
result, err := harness.Emit(extensions.InputEvent{
|
|
Text: "This is a normal message",
|
|
Source: "cli",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("expected nil result for regular input, got %v", result)
|
|
}
|
|
}
|
|
|
|
// Test complete session flow
|
|
func TestToolLogger_FullSession(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
// Simulate a full session
|
|
_, err := harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Several tool calls
|
|
tools := []string{"Read", "Glob", "Grep", "Bash"}
|
|
for _, tool := range tools {
|
|
_, err := harness.Emit(extensions.ToolCallEvent{
|
|
ToolName: tool,
|
|
Input: "{}",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error for tool %s: %v", tool, err)
|
|
}
|
|
|
|
_, err = harness.Emit(extensions.ToolResultEvent{
|
|
ToolName: tool,
|
|
Content: "result",
|
|
IsError: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error for tool result %s: %v", tool, err)
|
|
}
|
|
}
|
|
|
|
// User issues a command
|
|
_, err = harness.Emit(extensions.InputEvent{Text: "!time", Source: "cli"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = harness.Emit(extensions.SessionShutdownEvent{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify the !time command was handled
|
|
if len(harness.Context().GetPrintInfos()) != 1 {
|
|
t.Errorf("expected 1 PrintInfo call, got %d", len(harness.Context().GetPrintInfos()))
|
|
}
|
|
}
|
|
|
|
// Test that the extension handles file write errors gracefully
|
|
func TestToolLogger_FileError(t *testing.T) {
|
|
// This test verifies the extension doesn't panic when file operations fail
|
|
// Since we can't easily mock os.OpenFile, we rely on the extension code
|
|
// properly checking for errors (which it does)
|
|
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
// Just verify the handlers run without panicking
|
|
_, err := harness.Emit(extensions.ToolCallEvent{ToolName: "Read", Input: "{}"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test concurrent tool calls (race condition check)
|
|
func TestToolLogger_ConcurrentToolCalls(t *testing.T) {
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
// Run multiple tool calls concurrently
|
|
done := make(chan bool, 10)
|
|
for i := range 10 {
|
|
go func(index int) {
|
|
defer func() { done <- true }()
|
|
|
|
toolName := "Tool" + string(rune('0'+index))
|
|
_, err := harness.Emit(extensions.ToolCallEvent{
|
|
ToolName: toolName,
|
|
Input: "{}",
|
|
})
|
|
if err != nil {
|
|
t.Errorf("error in goroutine %d: %v", index, err)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for range 10 {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// Test the actual log file is created and written to
|
|
func TestToolLogger_LogFile(t *testing.T) {
|
|
logFile := "/tmp/kit-tool-log.txt"
|
|
|
|
// Clean up before test
|
|
_ = os.Remove(logFile)
|
|
|
|
harness := test.New(t)
|
|
harness.LoadFile("tool-logger.go")
|
|
|
|
// Emit events
|
|
_, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
|
|
_, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "Read", Input: "{}"})
|
|
_, _ = harness.Emit(extensions.ToolResultEvent{ToolName: "Read", Content: "data", IsError: false})
|
|
|
|
// Note: Since the extension writes to a real file and the test harness
|
|
// mocks the context, the file writes actually happen. Let's verify.
|
|
|
|
// Give it a moment for file operations
|
|
if _, err := os.Stat(logFile); err == nil {
|
|
// File exists - read and verify content
|
|
content, err := os.ReadFile(logFile)
|
|
if err != nil {
|
|
t.Logf("Could not read log file: %v", err)
|
|
} else {
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "SESSION_START") {
|
|
t.Error("log file should contain SESSION_START")
|
|
}
|
|
if !strings.Contains(contentStr, "CALL tool=Read") {
|
|
t.Error("log file should contain CALL tool=Read")
|
|
}
|
|
if !strings.Contains(contentStr, "RESULT tool=Read") {
|
|
t.Error("log file should contain RESULT tool=Read")
|
|
}
|
|
}
|
|
} else {
|
|
t.Log("Note: Log file not created - this is expected since the extension writes directly to disk")
|
|
}
|
|
}
|