Files
2026-06-02 23:14:41 +08:00

370 lines
10 KiB
Go

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
}