package config import ( "os" "os/exec" "path/filepath" "reflect" "testing" ) func TestCCSwitchRowsToPlugins(t *testing.T) { rows := []ccSwitchMCPRow{ {ID: "docs-id", Name: "docs", ServerConfig: `{"type":"http","url":"https://mcp.example.test","headers":{"Authorization":"Bearer ${TOKEN}"}}`}, {Name: "fs", ServerConfig: `{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]}`}, } got, err := ccSwitchRowsToPlugins(rows) if err != nil { t.Fatalf("ccSwitchRowsToPlugins: %v", err) } if got[0].Name != "docs-id" || got[0].Type != "http" || got[0].URL != "https://mcp.example.test" { t.Fatalf("http entry = %+v", got[0]) } if got[0].Headers["Authorization"] != "Bearer ${TOKEN}" { t.Errorf("header was not preserved: %+v", got[0].Headers) } if got[1].Name != "fs" || got[1].Command != "npx" || !reflect.DeepEqual(got[1].Args, []string{"-y", "@modelcontextprotocol/server-filesystem", "."}) { t.Fatalf("stdio entry = %+v", got[1]) } } func TestCCSwitchRowsPreferIDForDuplicateDisplayNames(t *testing.T) { rows := []ccSwitchMCPRow{ {ID: "search-code", Name: "search", ServerConfig: `{"command":"node","args":["code.js"]}`}, {ID: "search-docs", Name: "search", ServerConfig: `{"command":"node","args":["docs.js"]}`}, } got, err := ccSwitchRowsToPlugins(rows) if err != nil { t.Fatalf("ccSwitchRowsToPlugins: %v", err) } if got[0].Name != "search-code" || got[1].Name != "search-docs" { t.Fatalf("names = %q, %q; want stable ids", got[0].Name, got[1].Name) } } func TestCCSwitchImportClassifiesRiskyServers(t *testing.T) { rows := []ccSwitchMCPRow{ {Name: "@modelcontextprotocol/server-chrome-devtools", ServerConfig: `{"command":"npx","args":["-y","chrome-devtools-mcp@latest"]}`}, {Name: "legacy", ServerConfig: `{"type":"sse","url":"https://example.test/sse"}`}, } got, err := ccSwitchRowsToPlugins(rows) if err != nil { t.Fatalf("ccSwitchRowsToPlugins: %v", err) } for _, e := range got { candidate := classifyMCPImportCandidate(e) if candidate.Recommended { t.Fatalf("%s should not be recommended: %+v", e.Name, candidate) } } } func TestLoadCCSwitchLegacyConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json.migrated") body := `{ "mcp": { "servers": { "off": { "name": "off", "server": {"command": "node", "args": ["off.js"]}, "apps": {"codex": false} }, "time": { "name": "@modelcontextprotocol/server-time", "server": {"type":"stdio", "command": "uvx", "args": ["mcp-server-time"]}, "apps": {"codex": true} } } } }` if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } got, err := loadCCSwitchLegacyConfig(path) if err != nil { t.Fatalf("loadCCSwitchLegacyConfig: %v", err) } if len(got) != 1 { t.Fatalf("entries = %d, want 1: %+v", len(got), got) } if got[0].Name != "@modelcontextprotocol/server-time" || got[0].Command != "uvx" { t.Fatalf("entry = %+v", got[0]) } } func TestLoadCCSwitchMCPEmptyDBDoesNotReadLegacyBackups(t *testing.T) { if _, err := exec.LookPath("sqlite3"); err != nil { t.Skip("sqlite3 not available") } root := t.TempDir() dbPath := filepath.Join(root, "cc-switch.db") if out, err := exec.Command("sqlite3", dbPath, `CREATE TABLE mcp_servers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL, enabled_codex BOOLEAN NOT NULL DEFAULT 0 );`).CombinedOutput(); err != nil { t.Fatalf("create sqlite db: %v\n%s", err, out) } stale := `{"mcp":{"servers":{"stale":{"name":"stale","server":{"command":"node","args":["stale.js"]},"apps":{"codex":true}}}}}` if err := os.WriteFile(filepath.Join(root, "config.json.migrated"), []byte(stale), 0o644); err != nil { t.Fatal(err) } got, err := loadCCSwitchMCPFromRoot(root) if err != nil { t.Fatalf("loadCCSwitchMCPFromRoot: %v", err) } if len(got) != 0 { t.Fatalf("empty sqlite db should be authoritative, got legacy entries: %+v", got) } }