返回 DeepSeek-Reasonix
ccswitch_test.go
根目录 / internal / config / ccswitch_test.go
1 package config
2
3 import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "reflect"
8 "testing"
9 )
10
11 func TestCCSwitchRowsToPlugins(t *testing.T) {
12 rows := []ccSwitchMCPRow{
13 {ID: "docs-id", Name: "docs", ServerConfig: `{"type":"http","url":"https://mcp.example.test","headers":{"Authorization":"Bearer ${TOKEN}"}}`},
14 {Name: "fs", ServerConfig: `{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]}`},
15 }
16 got, err := ccSwitchRowsToPlugins(rows)
17 if err != nil {
18 t.Fatalf("ccSwitchRowsToPlugins: %v", err)
19 }
20 if got[0].Name != "docs-id" || got[0].Type != "http" || got[0].URL != "https://mcp.example.test" {
21 t.Fatalf("http entry = %+v", got[0])
22 }
23 if got[0].Headers["Authorization"] != "Bearer ${TOKEN}" {
24 t.Errorf("header was not preserved: %+v", got[0].Headers)
25 }
26 if got[1].Name != "fs" || got[1].Command != "npx" ||
27 !reflect.DeepEqual(got[1].Args, []string{"-y", "@modelcontextprotocol/server-filesystem", "."}) {
28 t.Fatalf("stdio entry = %+v", got[1])
29 }
30 }
31
32 func TestCCSwitchRowsPreferIDForDuplicateDisplayNames(t *testing.T) {
33 rows := []ccSwitchMCPRow{
34 {ID: "search-code", Name: "search", ServerConfig: `{"command":"node","args":["code.js"]}`},
35 {ID: "search-docs", Name: "search", ServerConfig: `{"command":"node","args":["docs.js"]}`},
36 }
37 got, err := ccSwitchRowsToPlugins(rows)
38 if err != nil {
39 t.Fatalf("ccSwitchRowsToPlugins: %v", err)
40 }
41 if got[0].Name != "search-code" || got[1].Name != "search-docs" {
42 t.Fatalf("names = %q, %q; want stable ids", got[0].Name, got[1].Name)
43 }
44 }
45
46 func TestCCSwitchImportClassifiesRiskyServers(t *testing.T) {
47 rows := []ccSwitchMCPRow{
48 {Name: "@modelcontextprotocol/server-chrome-devtools", ServerConfig: `{"command":"npx","args":["-y","chrome-devtools-mcp@latest"]}`},
49 {Name: "legacy", ServerConfig: `{"type":"sse","url":"https://example.test/sse"}`},
50 }
51 got, err := ccSwitchRowsToPlugins(rows)
52 if err != nil {
53 t.Fatalf("ccSwitchRowsToPlugins: %v", err)
54 }
55 for _, e := range got {
56 candidate := classifyMCPImportCandidate(e)
57 if candidate.Recommended {
58 t.Fatalf("%s should not be recommended: %+v", e.Name, candidate)
59 }
60 }
61 }
62
63 func TestLoadCCSwitchLegacyConfig(t *testing.T) {
64 dir := t.TempDir()
65 path := filepath.Join(dir, "config.json.migrated")
66 body := `{
67 "mcp": {
68 "servers": {
69 "off": {
70 "name": "off",
71 "server": {"command": "node", "args": ["off.js"]},
72 "apps": {"codex": false}
73 },
74 "time": {
75 "name": "@modelcontextprotocol/server-time",
76 "server": {"type":"stdio", "command": "uvx", "args": ["mcp-server-time"]},
77 "apps": {"codex": true}
78 }
79 }
80 }
81 }`
82 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
83 t.Fatal(err)
84 }
85 got, err := loadCCSwitchLegacyConfig(path)
86 if err != nil {
87 t.Fatalf("loadCCSwitchLegacyConfig: %v", err)
88 }
89 if len(got) != 1 {
90 t.Fatalf("entries = %d, want 1: %+v", len(got), got)
91 }
92 if got[0].Name != "@modelcontextprotocol/server-time" || got[0].Command != "uvx" {
93 t.Fatalf("entry = %+v", got[0])
94 }
95 }
96
97 func TestLoadCCSwitchLegacyConfigPrefersReasonixFlag(t *testing.T) {
98 dir := t.TempDir()
99 path := filepath.Join(dir, "config.json")
100 body := `{
101 "mcp": {
102 "servers": {
103 "legacy": {
104 "name": "legacy",
105 "server": {"command": "node", "args": ["legacy.js"]},
106 "apps": {"codex": true}
107 },
108 "reasonix-off": {
109 "name": "reasonix-off",
110 "server": {"command": "node", "args": ["off.js"]},
111 "apps": {"codex": true, "reasonix": false}
112 },
113 "reasonix-on": {
114 "name": "reasonix-on",
115 "server": {"command": "node", "args": ["on.js"]},
116 "apps": {"codex": false, "reasonix": true}
117 }
118 }
119 }
120 }`
121 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
122 t.Fatal(err)
123 }
124 got, err := loadCCSwitchLegacyConfig(path)
125 if err != nil {
126 t.Fatalf("loadCCSwitchLegacyConfig: %v", err)
127 }
128 if len(got) != 2 || got[0].Name != "legacy" || got[1].Name != "reasonix-on" {
129 t.Fatalf("entries = %+v, want legacy fallback and explicit Reasonix enablement", got)
130 }
131 }
132
133 func TestLoadCCSwitchMCPDBPrefersReasonixColumn(t *testing.T) {
134 if _, err := exec.LookPath("sqlite3"); err != nil {
135 t.Skip("sqlite3 not available")
136 }
137 dbPath := filepath.Join(t.TempDir(), "cc-switch.db")
138 setup := `CREATE TABLE mcp_servers (
139 id TEXT PRIMARY KEY,
140 name TEXT NOT NULL,
141 server_config TEXT NOT NULL,
142 enabled_codex BOOLEAN NOT NULL DEFAULT 0,
143 enabled_reasonix BOOLEAN NOT NULL DEFAULT 0
144 );
145 INSERT INTO mcp_servers VALUES ('codex-only', 'codex-only', '{"command":"node","args":["codex.js"]}', 1, 0);
146 INSERT INTO mcp_servers VALUES ('reasonix-only', 'reasonix-only', '{"command":"node","args":["reasonix.js"]}', 0, 1);`
147 if out, err := exec.Command("sqlite3", dbPath, setup).CombinedOutput(); err != nil {
148 t.Fatalf("create sqlite db: %v\n%s", err, out)
149 }
150 got, err := loadCCSwitchMCPDB(dbPath)
151 if err != nil {
152 t.Fatalf("loadCCSwitchMCPDB: %v", err)
153 }
154 if len(got) != 1 || got[0].Name != "reasonix-only" {
155 t.Fatalf("entries = %+v, want only explicit Reasonix enablement", got)
156 }
157 }
158
159 func TestLoadCCSwitchMCPDBFallsBackToCodexWithoutReasonixColumn(t *testing.T) {
160 if _, err := exec.LookPath("sqlite3"); err != nil {
161 t.Skip("sqlite3 not available")
162 }
163 dbPath := filepath.Join(t.TempDir(), "cc-switch.db")
164 setup := `CREATE TABLE mcp_servers (
165 id TEXT PRIMARY KEY,
166 name TEXT NOT NULL,
167 server_config TEXT NOT NULL,
168 enabled_codex BOOLEAN NOT NULL DEFAULT 0
169 );
170 INSERT INTO mcp_servers VALUES ('codex-on', 'codex-on', '{"command":"node","args":["on.js"]}', 1);
171 INSERT INTO mcp_servers VALUES ('codex-off', 'codex-off', '{"command":"node","args":["off.js"]}', 0);`
172 if out, err := exec.Command("sqlite3", dbPath, setup).CombinedOutput(); err != nil {
173 t.Fatalf("create sqlite db: %v\n%s", err, out)
174 }
175 got, err := loadCCSwitchMCPDB(dbPath)
176 if err != nil {
177 t.Fatalf("loadCCSwitchMCPDB: %v", err)
178 }
179 if len(got) != 1 || got[0].Name != "codex-on" {
180 t.Fatalf("entries = %+v, want legacy Codex fallback", got)
181 }
182 }
183
184 func TestLoadCCSwitchMCPEmptyDBDoesNotReadLegacyBackups(t *testing.T) {
185 if _, err := exec.LookPath("sqlite3"); err != nil {
186 t.Skip("sqlite3 not available")
187 }
188 root := t.TempDir()
189 dbPath := filepath.Join(root, "cc-switch.db")
190 if out, err := exec.Command("sqlite3", dbPath, `CREATE TABLE mcp_servers (
191 id TEXT PRIMARY KEY,
192 name TEXT NOT NULL,
193 server_config TEXT NOT NULL,
194 enabled_codex BOOLEAN NOT NULL DEFAULT 0
195 );`).CombinedOutput(); err != nil {
196 t.Fatalf("create sqlite db: %v\n%s", err, out)
197 }
198 stale := `{"mcp":{"servers":{"stale":{"name":"stale","server":{"command":"node","args":["stale.js"]},"apps":{"codex":true}}}}}`
199 if err := os.WriteFile(filepath.Join(root, "config.json.migrated"), []byte(stale), 0o644); err != nil {
200 t.Fatal(err)
201 }
202 got, err := loadCCSwitchMCPFromRoot(root)
203 if err != nil {
204 t.Fatalf("loadCCSwitchMCPFromRoot: %v", err)
205 }
206 if len(got) != 0 {
207 t.Fatalf("empty sqlite db should be authoritative, got legacy entries: %+v", got)
208 }
209 }
210
210 lines GO