package aichannel
import (
"context"
"os"
"testing"
"time"
)
func TestNew(t *testing.T) {
ch := New()
if ch == nil {
t.Fatal("New() returned nil")
}
if ch.configured {
t.Error("New channel should not be configured")
}
}
func TestNewWithConfig(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Timeout: 30 * time.Second,
})
if ch == nil {
t.Fatal("NewWithConfig() returned nil")
}
if !ch.configured {
t.Error("Channel should be configured")
}
}
func TestConfigure_AppliesDefaults(t *testing.T) {
tests := []struct {
name string
agent AgentType
expectedCommand string
expectedNonIntFlag string
expectedQuietFlag string
expectedUseStdin bool
}{
{
name: "Claude",
agent: AgentClaude,
expectedCommand: "claude",
expectedNonIntFlag: "-p",
expectedQuietFlag: "",
expectedUseStdin: true,
},
{
name: "Copilot",
agent: AgentCopilot,
expectedCommand: "copilot",
expectedNonIntFlag: "-p",
expectedQuietFlag: "-s",
expectedUseStdin: true,
},
{
name: "Gemini",
agent: AgentGemini,
expectedCommand: "gemini",
expectedNonIntFlag: "-e",
expectedQuietFlag: "-q",
expectedUseStdin: true,
},
{
name: "Kimi",
agent: AgentKimi,
expectedCommand: "kimi-cli",
expectedNonIntFlag: "",
expectedQuietFlag: "",
expectedUseStdin: true,
},
{
name: "Cursor",
agent: AgentCursor,
expectedCommand: "cursor-agent",
expectedNonIntFlag: "-p",
expectedQuietFlag: "",
expectedUseStdin: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ch := NewWithConfig(Config{Agent: tt.agent})
cfg := ch.Config()
if cfg.Command != tt.expectedCommand {
t.Errorf("Command = %q, want %q", cfg.Command, tt.expectedCommand)
}
if cfg.NonInteractiveFlag != tt.expectedNonIntFlag {
t.Errorf("NonInteractiveFlag = %q, want %q", cfg.NonInteractiveFlag, tt.expectedNonIntFlag)
}
if cfg.QuietFlag != tt.expectedQuietFlag {
t.Errorf("QuietFlag = %q, want %q", cfg.QuietFlag, tt.expectedQuietFlag)
}
if cfg.UseStdin != tt.expectedUseStdin {
t.Errorf("UseStdin = %v, want %v", cfg.UseStdin, tt.expectedUseStdin)
}
})
}
}
func TestConfigure_CustomCommand(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Command: "/custom/path/claude",
})
cfg := ch.Config()
if cfg.Command != "/custom/path/claude" {
t.Errorf("Custom command not preserved: got %q", cfg.Command)
}
}
func TestConfigure_DefaultTimeout(t *testing.T) {
ch := NewWithConfig(Config{Agent: AgentClaude})
cfg := ch.Config()
if cfg.Timeout != 2*time.Minute {
t.Errorf("Timeout = %v, want 2 minutes", cfg.Timeout)
}
}
func TestConfigure_CustomTimeout(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Timeout: 5 * time.Minute,
})
cfg := ch.Config()
if cfg.Timeout != 5*time.Minute {
t.Errorf("Custom timeout not preserved: got %v", cfg.Timeout)
}
}
func TestIsAvailable_NotConfigured(t *testing.T) {
ch := New()
if ch.IsAvailable() {
t.Error("Unconfigured channel should not be available")
}
}
func TestIsAvailable_MissingCommand(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "nonexistent-ai-tool-12345",
})
if ch.IsAvailable() {
t.Error("Channel with missing command should not be available")
}
}
func TestIsAvailable_Echo(t *testing.T) {
// Echo should be available on all systems
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "echo",
})
if !ch.IsAvailable() {
t.Error("Channel with 'echo' command should be available")
}
}
func TestSend_NotConfigured(t *testing.T) {
ch := New()
_, err := ch.Send(context.Background(), "test", "")
if err != ErrNotConfigured {
t.Errorf("Expected ErrNotConfigured, got %v", err)
}
}
func TestSend_NotAvailable(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "nonexistent-ai-tool-12345",
})
_, err := ch.Send(context.Background(), "test", "")
if err == nil {
t.Error("Expected error for unavailable command")
}
}
func TestSend_WithEcho(t *testing.T) {
// Use echo as a mock AI agent
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "echo",
NonInteractiveFlag: "", // echo doesn't need flags
})
result, err := ch.Send(context.Background(), "hello world", "")
if err != nil {
t.Fatalf("Send() error = %v", err)
}
if result != "hello world" {
t.Errorf("Send() = %q, want %q", result, "hello world")
}
}
func TestSendAndParse_TextFormat(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "echo",
NonInteractiveFlag: "",
OutputFormat: "text",
})
resp, err := ch.SendAndParse(context.Background(), "hello world", "")
if err != nil {
t.Fatalf("SendAndParse() error = %v", err)
}
if resp.Result != "hello world" {
t.Errorf("Result = %q, want %q", resp.Result, "hello world")
}
}
func TestSendAndParse_JSONFormat(t *testing.T) {
// Use printf to output JSON
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "printf",
NonInteractiveFlag: "",
OutputFormat: "json",
OutputFormatFlag: "--output-format", // Enable JSON support for custom agent
})
// printf outputs JSON literally
resp, err := ch.SendAndParse(context.Background(), `{"type":"result","result":"JSON response","session_id":"test123"}`, "")
if err != nil {
t.Fatalf("SendAndParse() error = %v", err)
}
if resp.Result != "JSON response" {
t.Errorf("Result = %q, want %q", resp.Result, "JSON response")
}
if resp.SessionID != "test123" {
t.Errorf("SessionID = %q, want %q", resp.SessionID, "test123")
}
}
func TestSendAndParse_DefaultsToText(t *testing.T) {
// No OutputFormat specified - should default to text
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "echo",
NonInteractiveFlag: "",
// OutputFormat not set
})
resp, err := ch.SendAndParse(context.Background(), "plain text", "")
if err != nil {
t.Fatalf("SendAndParse() error = %v", err)
}
if resp.Result != "plain text" {
t.Errorf("Result = %q, want %q", resp.Result, "plain text")
}
}
func TestSend_WithStdin(t *testing.T) {
// Use cat as a mock that reads stdin
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "cat",
NonInteractiveFlag: "",
UseStdin: true,
})
result, err := ch.Send(context.Background(), "", "stdin content")
if err != nil {
t.Fatalf("Send() error = %v", err)
}
if result != "stdin content" {
t.Errorf("Send() = %q, want %q", result, "stdin content")
}
}
func TestSend_Timeout(t *testing.T) {
// Use sleep to test timeout
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "sleep",
NonInteractiveFlag: "",
Timeout: 100 * time.Millisecond,
})
_, err := ch.Send(context.Background(), "10", "")
if err != ErrTimeout {
t.Errorf("Expected ErrTimeout, got %v", err)
}
}
func TestBuildArgs_Claude(t *testing.T) {
ch := NewWithConfig(Config{Agent: AgentClaude})
args := ch.buildArgs("test prompt")
// Should have: -p "test prompt"
found := false
for i, arg := range args {
if arg == "-p" && i+1 < len(args) && args[i+1] == "test prompt" {
found = true
break
}
}
if !found {
t.Errorf("Expected -p flag with prompt, got %v", args)
}
}
func TestBuildArgs_Copilot(t *testing.T) {
ch := NewWithConfig(Config{Agent: AgentCopilot})
args := ch.buildArgs("test prompt")
// Should have: -p "test prompt" -s
hasP := false
hasS := false
for i, arg := range args {
if arg == "-p" && i+1 < len(args) && args[i+1] == "test prompt" {
hasP = true
}
if arg == "-s" {
hasS = true
}
}
if !hasP {
t.Errorf("Expected -p flag with prompt, got %v", args)
}
if !hasS {
t.Errorf("Expected -s (quiet) flag, got %v", args)
}
}
func TestBuildArgs_WithExtraArgs(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Args: []string{"--model", "opus"},
})
args := ch.buildArgs("test")
// Extra args should come first
if len(args) < 2 || args[0] != "--model" || args[1] != "opus" {
t.Errorf("Extra args not at beginning: %v", args)
}
}
func TestBuildArgs_OutputFormat(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
OutputFormat: "json",
})
args := ch.buildArgs("test")
found := false
for i, arg := range args {
if arg == "--output-format" && i+1 < len(args) && args[i+1] == "json" {
found = true
break
}
}
if !found {
t.Errorf("Expected --output-format json, got %v", args)
}
}
func TestBuildArgs_OutputFormat_UnsupportedAgent(t *testing.T) {
// Copilot doesn't support --output-format
ch := NewWithConfig(Config{
Agent: AgentCopilot,
OutputFormat: "json",
})
args := ch.buildArgs("test")
// Should NOT have --output-format flag
for _, arg := range args {
if arg == "--output-format" {
t.Errorf("Copilot should not have --output-format flag, got %v", args)
}
}
}
func TestSupportsJSONOutput(t *testing.T) {
tests := []struct {
agent AgentType
expected bool
}{
{AgentClaude, true},
{AgentGemini, true},
{AgentCopilot, false},
{AgentKimi, false},
{AgentAider, false},
{AgentOpenCode, false},
{AgentCursor, false},
}
for _, tt := range tests {
t.Run(string(tt.agent), func(t *testing.T) {
ch := NewWithConfig(Config{Agent: tt.agent})
if ch.SupportsJSONOutput() != tt.expected {
t.Errorf("SupportsJSONOutput() = %v, want %v", ch.SupportsJSONOutput(), tt.expected)
}
})
}
}
func TestGetKnownAgents(t *testing.T) {
agents := GetKnownAgents()
if len(agents) == 0 {
t.Error("GetKnownAgents() returned empty list")
}
// Check that known agents are in the list
expected := map[AgentType]bool{
AgentClaude: true,
AgentCopilot: true,
AgentGemini: true,
}
for agent := range expected {
found := false
for _, a := range agents {
if a == agent {
found = true
break
}
}
if !found {
t.Errorf("Expected agent %s not in list", agent)
}
}
}
func TestDetectAvailableAgents(t *testing.T) {
// This just tests that the function runs without error
// The result depends on what's installed
agents := DetectAvailableAgents()
t.Logf("Detected available agents: %v", agents)
}
func TestStripANSI(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Plain text",
input: "Hello World",
expected: "Hello World",
},
{
name: "CSI color sequence",
input: "\x1b[31mRed\x1b[0m Text",
expected: "Red Text",
},
{
name: "Cursor movement",
input: "\x1b[2J\x1b[HHello",
expected: "Hello",
},
{
name: "Show cursor",
input: "Text\x1b[?25h",
expected: "Text",
},
{
name: "Carriage return removal",
input: "Line1\r\nLine2",
expected: "Line1\nLine2",
},
{
name: "OSC sequence with BEL",
input: "\x1b]0;Title\x07Content",
expected: "Content",
},
{
name: "Multiple sequences",
input: "\x1b[32mGreen\x1b[0m and \x1b[34mBlue\x1b[0m",
expected: "Green and Blue",
},
{
name: "Empty string",
input: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stripANSI(tt.input)
if result != tt.expected {
t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestConfigure_ClaudePTY(t *testing.T) {
ch := New()
ch.Configure(Config{Agent: AgentClaude})
if !ch.config.UsePTY {
t.Error("Claude should have UsePTY enabled by default")
}
}
func TestSend_WithPTY(t *testing.T) {
// Test PTY mode with echo command
ch := NewWithConfig(Config{
Agent: AgentCustom,
Command: "echo",
NonInteractiveFlag: "",
UsePTY: true,
Timeout: 5 * time.Second,
})
result, err := ch.Send(context.Background(), "hello from PTY", "")
if err != nil {
t.Fatalf("Send with PTY failed: %v", err)
}
if result != "hello from PTY" {
t.Errorf("Expected 'hello from PTY', got %q", result)
}
}
// API Mode Tests
func TestIsAPIMode(t *testing.T) {
t.Run("CLI mode", func(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
})
if ch.IsAPIMode() {
t.Error("Expected CLI mode, got API mode")
}
})
t.Run("API mode", func(t *testing.T) {
ch := NewWithConfig(Config{
UseAPI: true,
APIKey: "test-key",
})
if !ch.IsAPIMode() {
t.Error("Expected API mode, got CLI mode")
}
})
}
func TestConfigure_APIMode(t *testing.T) {
ch := NewWithConfig(Config{
UseAPI: true,
APIKey: "test-key",
Model: "claude-haiku-3-5-20241022",
MaxTokens: 512,
SystemPrompt: "You are a test assistant.",
})
cfg := ch.Config()
if !cfg.UseAPI {
t.Error("UseAPI should be true")
}
if cfg.APIKey != "test-key" {
t.Errorf("APIKey = %q, want %q", cfg.APIKey, "test-key")
}
if cfg.Model != "claude-haiku-3-5-20241022" {
t.Errorf("Model = %q, want %q", cfg.Model, "claude-haiku-3-5-20241022")
}
if cfg.MaxTokens != 512 {
t.Errorf("MaxTokens = %d, want %d", cfg.MaxTokens, 512)
}
if cfg.SystemPrompt != "You are a test assistant." {
t.Errorf("SystemPrompt = %q, want %q", cfg.SystemPrompt, "You are a test assistant.")
}
}
func TestIsAvailable_APIMode_Configured(t *testing.T) {
ch := NewWithConfig(Config{
UseAPI: true,
APIKey: "test-key",
})
if !ch.IsAvailable() {
t.Error("API mode channel with key should be available")
}
}
func TestIsAvailable_APIMode_NoKey(t *testing.T) {
// Save and unset ALL provider env vars
envVars := []string{
"ANTHROPIC_API_KEY", "CLAUDE_KEY",
"OPENAI_KEY", "OPENAI_API_KEY",
"GOOGLE_KEY", "GOOGLE_API_KEY",
"MISTRAL_KEY", "MISTRAL_API_KEY",
"DEEP_SEEK_KEY", "DEEPSEEK_API_KEY",
"OPEN_ROUTER_KEY", "OPENROUTER_API_KEY",
"TOGETHER_KEY", "TOGETHER_API_KEY",
"HYPERBOLIC_KEY", "HYPERBOLIC_API_KEY",
"SAMBA_NOVA_KEY", "SAMBANOVA_API_KEY",
"GLM_KEY", "GLM_API_KEY",
}
originals := make(map[string]string)
for _, key := range envVars {
originals[key] = os.Getenv(key)
os.Unsetenv(key)
}
defer func() {
for key, val := range originals {
if val != "" {
os.Setenv(key, val)
}
}
}()
ch := NewWithConfig(Config{
UseAPI: true,
// No APIKey provided and no env vars
})
if ch.IsAvailable() {
t.Error("API mode channel without key should not be available")
}
}
func TestSend_APIMode_NoProvider(t *testing.T) {
// Create a channel without proper API setup
ch := &Channel{
config: Config{
UseAPI: true,
},
configured: true,
provider: nil, // No provider
}
_, err := ch.Send(context.Background(), "test", "")
if err == nil {
t.Error("Expected error when provider is nil")
}
}
func TestSendAndParse_APIMode_NoProvider(t *testing.T) {
// Create a channel without proper API setup
ch := &Channel{
config: Config{
UseAPI: true,
},
configured: true,
provider: nil, // No provider
}
_, err := ch.SendAndParse(context.Background(), "test", "")
if err == nil {
t.Error("Expected error when provider is nil")
}
}
func TestCreateProvider(t *testing.T) {
// Test with explicit provider
ch := NewWithConfig(Config{
UseAPI: true,
LLMProvider: ProviderOpenAI,
APIKey: "test-key",
Model: "gpt-4o-mini",
MaxTokens: 512,
})
if ch.provider == nil {
t.Fatal("Expected provider to be created")
}
if ch.provider.Name() != "openai" {
t.Errorf("Provider name = %q, want %q", ch.provider.Name(), "openai")
}
}
func TestCreateProvider_AutoDetect(t *testing.T) {
// Test that provider is auto-detected when API keys are available
ch := NewWithConfig(Config{
UseAPI: true,
APIKey: "test-key", // This won't be used if env vars are set
})
// Provider should be created if any env vars are set
available := GetAvailableProviders()
if len(available) > 0 {
if ch.provider == nil {
t.Fatal("Expected provider to be created when env vars are available")
}
// Should match default provider
defaultProvider := GetDefaultProvider()
if ch.provider.Name() != string(defaultProvider) {
t.Logf("Provider auto-detected: %s (default: %s)", ch.provider.Name(), defaultProvider)
}
}
}
// Model Configuration Tests
func TestConfigure_ClaudeDefaultModel(t *testing.T) {
ch := NewWithConfig(Config{Agent: AgentClaude})
cfg := ch.Config()
// Claude should default to haiku model
if cfg.Model != "haiku" {
t.Errorf("Claude Model = %q, want %q", cfg.Model, "haiku")
}
}
func TestConfigure_ClaudeCustomModel(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Model: "opus",
})
cfg := ch.Config()
// Custom model should be preserved
if cfg.Model != "opus" {
t.Errorf("Claude Model = %q, want %q", cfg.Model, "opus")
}
}
func TestBuildArgs_ClaudeWithModel(t *testing.T) {
ch := NewWithConfig(Config{Agent: AgentClaude})
args := ch.buildArgs("test prompt")
// Should have: --model haiku -p "test prompt"
foundModel := false
for i, arg := range args {
if arg == "--model" && i+1 < len(args) && args[i+1] == "haiku" {
foundModel = true
break
}
}
if !foundModel {
t.Errorf("Expected --model haiku, got %v", args)
}
}
func TestBuildArgs_ClaudeCustomModel(t *testing.T) {
ch := NewWithConfig(Config{
Agent: AgentClaude,
Model: "sonnet",
})
args := ch.buildArgs("test prompt")
// Should have: --model sonnet
foundModel := false
for i, arg := range args {
if arg == "--model" && i+1 < len(args) && args[i+1] == "sonnet" {
foundModel = true
break
}
}
if !foundModel {
t.Errorf("Expected --model sonnet, got %v", args)
}
}
func TestBuildArgs_NonClaudeNoModelFlag(t *testing.T) {
// Non-Claude agents should not get --model flag even if Model is set
ch := NewWithConfig(Config{
Agent: AgentCopilot,
Model: "some-model",
})
args := ch.buildArgs("test prompt")
for _, arg := range args {
if arg == "--model" {
t.Errorf("Non-Claude agent should not have --model flag, got %v", args)
}
}
}
// Error Response Handling Tests
func TestSendAndParse_ErrorResponse(t *testing.T) {
// Test that error responses are correctly parsed with IsError flag
errorJSON := `{"type":"result","subtype":"error","is_error":true,"result":"Something went wrong"}`
resp, err := ParseResponse(errorJSON, OutputFormatJSON)
if err != nil {
t.Fatalf("ParseResponse failed: %v", err)
}
if !resp.IsError {
t.Error("Expected IsError to be true")
}
if resp.Subtype != "error" {
t.Errorf("Subtype = %q, want %q", resp.Subtype, "error")
}
}
func TestSendAndParse_SuccessResponse(t *testing.T) {
// Test that success responses don't return error
successJSON := `{"type":"result","subtype":"success","is_error":false,"result":"All good"}`
resp, err := ParseResponse(successJSON, OutputFormatJSON)
if err != nil {
t.Fatalf("ParseResponse failed: %v", err)
}
if resp.IsError {
t.Error("Expected IsError to be false")
}
if resp.Subtype != "success" {
t.Errorf("Subtype = %q, want %q", resp.Subtype, "success")
}
if resp.Result != "All good" {
t.Errorf("Result = %q, want %q", resp.Result, "All good")
}
}
func TestErrAgentError(t *testing.T) {
// Test that ErrAgentError can be used with errors.Is
if ErrAgentError == nil {
t.Fatal("ErrAgentError should not be nil")
}
if ErrAgentError.Error() != "agent error" {
t.Errorf("ErrAgentError.Error() = %q, want %q", ErrAgentError.Error(), "agent error")
}
}