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") } }