package copilot import ( "context" "os" "testing" "time" "github.com/chenhg5/cc-connect/core" ) func TestNew_MissingBinary(t *testing.T) { _, err := New(map[string]any{ "cli_path": "copilot-nonexistent-binary-xyz", }) if err == nil { t.Fatal("expected error for missing binary") } } func TestAgent_Name(t *testing.T) { a := &Agent{} if a.Name() != "copilot" { t.Fatalf("Name() = %q, want copilot", a.Name()) } } func TestNormalizeMode(t *testing.T) { tests := []struct { input string want string }{ {"default", "default"}, {"bypassPermissions", "bypassPermissions"}, {"bypass-permissions", "bypassPermissions"}, {"bypass_permissions", "bypassPermissions"}, {"yolo", "bypassPermissions"}, {"YOLO", "bypassPermissions"}, {"", "default"}, {"unknown", "default"}, } for _, tt := range tests { got := normalizeMode(tt.input) if got != tt.want { t.Errorf("normalizeMode(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestAgent_SetGetModel(t *testing.T) { a := &Agent{} a.SetModel("gpt-4o") if got := a.GetModel(); got != "gpt-4o" { t.Fatalf("GetModel() = %q, want gpt-4o", got) } } func TestAgent_SetGetMode(t *testing.T) { a := &Agent{} a.SetMode("yolo") if got := a.GetMode(); got != "bypassPermissions" { t.Fatalf("GetMode() = %q, want bypassPermissions", got) } a.SetMode("default") if got := a.GetMode(); got != "default" { t.Fatalf("GetMode() = %q, want default", got) } } func TestAgent_SetGetWorkDir(t *testing.T) { a := &Agent{} a.SetWorkDir("/tmp/test") if got := a.GetWorkDir(); got != "/tmp/test" { t.Fatalf("GetWorkDir() = %q, want /tmp/test", got) } } func TestAgent_PermissionModes(t *testing.T) { a := &Agent{} modes := a.PermissionModes() if len(modes) != 2 { t.Fatalf("PermissionModes() len = %d, want 2", len(modes)) } if modes[0].Key != "default" { t.Errorf("modes[0].Key = %q, want default", modes[0].Key) } if modes[1].Key != "bypassPermissions" { t.Errorf("modes[1].Key = %q, want bypassPermissions", modes[1].Key) } } func TestAgent_DeleteSession(t *testing.T) { a := &Agent{} err := a.DeleteSession(context.Background(), "some-session-id") if err != nil { t.Fatalf("DeleteSession() = %v, want nil", err) } } func TestAgent_GetSessionHistory(t *testing.T) { a := &Agent{} entries, err := a.GetSessionHistory(context.Background(), "some-session-id", 10) if err != nil { t.Fatalf("GetSessionHistory() error = %v", err) } if entries != nil { t.Fatalf("GetSessionHistory() = %v, want nil", entries) } } func TestAgent_CompressCommand(t *testing.T) { a := &Agent{} if got := a.CompressCommand(); got != "" { t.Fatalf("CompressCommand() = %q, want empty", got) } } func TestAgent_AvailableModels(t *testing.T) { a := &Agent{} models := a.AvailableModels(context.Background()) if len(models) == 0 { t.Fatal("AvailableModels() returned empty") } } func TestAgent_ProviderSwitcher(t *testing.T) { a := &Agent{activeIdx: -1} if p := a.GetActiveProvider(); p != nil { t.Fatalf("GetActiveProvider() = %v, want nil", p) } if got := a.SetActiveProvider("nonexistent"); got { t.Fatal("SetActiveProvider(nonexistent) = true, want false") } } func TestAgent_ProviderConfigLocked(t *testing.T) { a := &Agent{activeIdx: -1} a.SetProviders([]core.ProviderConfig{ { Name: "byok", APIKey: "sk-test", BaseURL: "https://provider.example/v1", Model: "gpt-5.2", CodexWireAPI: "responses", CodexHTTPHeaders: map[string]string{"X-Provider": "test"}, Env: map[string]string{ "COPILOT_PROVIDER_TYPE": "anthropic", "COPILOT_PROVIDER_WIRE_MODEL": "deployment-1", "COPILOT_PROVIDER_BEARER_TOKEN": "bearer-token", }, }, }) if !a.SetActiveProvider("byok") { t.Fatal("SetActiveProvider(byok) = false, want true") } provider := a.providerConfigLocked() if provider == nil { t.Fatal("providerConfigLocked() = nil") } if provider.Type != "anthropic" { t.Fatalf("Type = %q, want anthropic", provider.Type) } if provider.BaseURL != "https://provider.example/v1" { t.Fatalf("BaseURL = %q", provider.BaseURL) } if provider.APIKey != "sk-test" { t.Fatalf("APIKey = %q", provider.APIKey) } if provider.ModelID != "gpt-5.2" { t.Fatalf("ModelID = %q", provider.ModelID) } if provider.WireAPI != "responses" { t.Fatalf("WireAPI = %q", provider.WireAPI) } if provider.WireModel != "deployment-1" { t.Fatalf("WireModel = %q", provider.WireModel) } if provider.BearerToken != "bearer-token" { t.Fatalf("BearerToken = %q", provider.BearerToken) } if provider.Headers["X-Provider"] != "test" { t.Fatalf("Headers = %v", provider.Headers) } } func TestAgent_ProviderEnvLocked(t *testing.T) { a := &Agent{activeIdx: -1} a.SetProviders([]core.ProviderConfig{ { Name: "byok", APIKey: "sk-test", BaseURL: "https://provider.example/v1", Model: "gpt-5.2", CodexWireAPI: "responses", Env: map[string]string{"EXTRA": "1"}, }, }) if !a.SetActiveProvider("byok") { t.Fatal("SetActiveProvider(byok) = false, want true") } env := envMap(a.providerEnvLocked()) if env["COPILOT_PROVIDER_BASE_URL"] != "https://provider.example/v1" { t.Fatalf("COPILOT_PROVIDER_BASE_URL = %q", env["COPILOT_PROVIDER_BASE_URL"]) } if env["COPILOT_PROVIDER_API_KEY"] != "sk-test" { t.Fatalf("COPILOT_PROVIDER_API_KEY = %q", env["COPILOT_PROVIDER_API_KEY"]) } if env["COPILOT_MODEL"] != "gpt-5.2" { t.Fatalf("COPILOT_MODEL = %q", env["COPILOT_MODEL"]) } if env["COPILOT_PROVIDER_WIRE_API"] != "responses" { t.Fatalf("COPILOT_PROVIDER_WIRE_API = %q", env["COPILOT_PROVIDER_WIRE_API"]) } if env["EXTRA"] != "1" { t.Fatalf("EXTRA = %q", env["EXTRA"]) } } func TestAgent_ListSessions(t *testing.T) { a := &Agent{} sessions, err := a.ListSessions(context.Background()) if err != nil { t.Fatalf("ListSessions() error = %v", err) } if sessions != nil { t.Fatalf("ListSessions() = %v, want nil", sessions) } } func TestAgent_CLIBinaryName(t *testing.T) { a := &Agent{cliBin: "copilot"} if got := a.CLIBinaryName(); got != "copilot" { t.Fatalf("CLIBinaryName() = %q, want copilot", got) } } func TestAgent_CLIDisplayName(t *testing.T) { a := &Agent{} if got := a.CLIDisplayName(); got != "GitHub Copilot" { t.Fatalf("CLIDisplayName() = %q, want 'GitHub Copilot'", got) } } func TestAgent_WorkspaceAgentOptions(t *testing.T) { a := &Agent{mode: "bypassPermissions", model: "gpt-4o", cliBin: "copilot"} opts := a.WorkspaceAgentOptions() if opts["mode"] != "bypassPermissions" { t.Fatalf("mode = %v, want bypassPermissions", opts["mode"]) } if opts["model"] != "gpt-4o" { t.Fatalf("model = %v, want gpt-4o", opts["model"]) } // cliBin is "copilot" (default) so cli_path should not be set if _, ok := opts["cli_path"]; ok { t.Fatal("cli_path should not be set for default binary name") } // Custom CLI path should be included a2 := &Agent{mode: "default", cliBin: "/custom/copilot"} opts2 := a2.WorkspaceAgentOptions() if opts2["cli_path"] != "/custom/copilot" { t.Fatalf("cli_path = %v, want /custom/copilot", opts2["cli_path"]) } } // --------------------------------------------------------------------------- // ListSessions / DeleteSession tests using a fake process helper // --------------------------------------------------------------------------- // TestAgent_ListSessions_NoBinary verifies graceful nil return when binary is missing. func TestAgent_ListSessions_NoBinary(t *testing.T) { a := &Agent{cliBin: "copilot-nonexistent-xyz", workDir: "."} sessions, err := a.ListSessions(context.Background()) if err != nil { t.Fatalf("ListSessions with missing binary returned error: %v", err) } if sessions != nil { t.Fatalf("expected nil sessions, got %v", sessions) } } // TestAgent_DeleteSession_NoBinary verifies graceful nil return when binary is missing. func TestAgent_DeleteSession_NoBinary(t *testing.T) { a := &Agent{cliBin: "copilot-nonexistent-xyz", workDir: "."} if err := a.DeleteSession(context.Background(), "some-session-id"); err != nil { t.Fatalf("DeleteSession with missing binary returned error: %v", err) } } // TestAgent_DeleteSession_EmptyID verifies empty session ID returns nil immediately. func TestAgent_DeleteSession_EmptyID(t *testing.T) { a := &Agent{cliBin: "copilot-nonexistent-xyz", workDir: "."} if err := a.DeleteSession(context.Background(), ""); err != nil { t.Fatalf("DeleteSession with empty ID returned error: %v", err) } } // TestAgent_ListSessions_RPC tests ListSessions by using a real fake-process // approach: we re-exec the test binary as a "mock copilot" that speaks the // JSON-RPC protocol over stdio and responds to ping + session.list. // // The fake process is selected by the CC_MOCK_COPILOT_MODE env var. func TestAgent_ListSessions_RPC(t *testing.T) { bin, err := os.Executable() if err != nil { t.Skip("cannot get test executable path") } // Point agent at the test binary itself acting as a mock copilot a := &Agent{ cliBin: bin, workDir: ".", } t.Setenv("CC_MOCK_COPILOT_MODE", "list_sessions") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() sessions, err := a.ListSessions(ctx) if err != nil { t.Fatalf("ListSessions returned error: %v", err) } if len(sessions) != 2 { t.Fatalf("expected 2 sessions, got %d: %v", len(sessions), sessions) } if sessions[0].ID != "sess-001" { t.Fatalf("sessions[0].ID = %q, want sess-001", sessions[0].ID) } if sessions[1].ID != "sess-002" { t.Fatalf("sessions[1].ID = %q, want sess-002", sessions[1].ID) } } // TestAgent_DeleteSession_RPC tests DeleteSession using the same mock process approach. func TestAgent_DeleteSession_RPC(t *testing.T) { bin, err := os.Executable() if err != nil { t.Skip("cannot get test executable path") } a := &Agent{ cliBin: bin, workDir: ".", } t.Setenv("CC_MOCK_COPILOT_MODE", "delete_session") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := a.DeleteSession(ctx, "sess-001"); err != nil { t.Fatalf("DeleteSession returned error: %v", err) } } func envMap(env []string) map[string]string { out := make(map[string]string, len(env)) for _, entry := range env { for i := range entry { if entry[i] == '=' { out[entry[:i]] = entry[i+1:] break } } } return out }