package server_test import ( "cursor-api-proxy/internal/config" "cursor-api-proxy/internal/server" "encoding/json" "fmt" "io" "net" "context" "net/http" "os" "strings" "testing" "time" ) // freePort 取得一個暫時可用的隨機 port func freePort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } port := l.Addr().(*net.TCPAddr).Port l.Close() return port } // makeFakeAgentBin 建立一個 shell script,模擬 agent 固定輸出 // sync 模式:直接輸出一行文字 // stream 模式:輸出 JSON stream 行 func makeFakeAgentBin(t *testing.T, syncOutput string) string { t.Helper() dir := t.TempDir() script := dir + "/agent" content := fmt.Sprintf(`#!/bin/sh # 若有 --stream-json 則輸出 stream 格式 for arg; do if [ "$arg" = "--stream-json" ]; then printf '%%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"%s"}]}}' printf '%%s\n' '{"type":"result","subtype":"success"}' exit 0 fi done # 否則輸出 sync 格式 printf '%%s' '%s' `, syncOutput, syncOutput) if err := os.WriteFile(script, []byte(content), 0755); err != nil { t.Fatal(err) } return script } // makeFakeAgentBinWithModels 額外支援 --list-models 輸出 func makeFakeAgentBinWithModels(t *testing.T) string { t.Helper() dir := t.TempDir() script := dir + "/agent" content := `#!/bin/sh for arg; do if [ "$arg" = "--list-models" ]; then printf 'claude-3-opus - Claude 3 Opus\n' printf 'claude-3-sonnet - Claude 3 Sonnet\n' exit 0 fi if [ "$arg" = "--stream-json" ]; then printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]}}' printf '%s\n' '{"type":"result","subtype":"success"}' exit 0 fi done printf 'Hello from agent' ` if err := os.WriteFile(script, []byte(content), 0755); err != nil { t.Fatal(err) } return script } func makeTestConfig(agentBin string, port int, overrides ...func(*config.BridgeConfig)) config.BridgeConfig { cfg := config.BridgeConfig{ AgentBin: agentBin, Host: "127.0.0.1", Port: port, DefaultModel: "auto", Mode: "ask", Force: false, ApproveMcps: false, StrictModel: true, Workspace: os.TempDir(), TimeoutMs: 30000, SessionsLogPath: os.TempDir() + "/test-sessions.log", ChatOnlyWorkspace: true, Verbose: false, MaxMode: false, ConfigDirs: []string{}, MultiPort: false, WinCmdlineMax: 30000, } for _, fn := range overrides { fn(&cfg) } return cfg } func waitListening(t *testing.T, host string, port int, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 50*time.Millisecond) if err == nil { conn.Close() return } time.Sleep(20 * time.Millisecond) } t.Fatalf("server on port %d did not start within %v", port, timeout) } func doRequest(t *testing.T, method, url, body string, headers map[string]string) (int, string) { t.Helper() var reqBody io.Reader if body != "" { reqBody = strings.NewReader(body) } req, err := http.NewRequest(method, url, reqBody) if err != nil { t.Fatal(err) } if body != "" { req.Header.Set("Content-Type", "application/json") } for k, v := range headers { req.Header.Set(k, v) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(data) } func TestBridgeServer_Health(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBinWithModels(t) cfg := makeTestConfig(agentBin, port) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() status, body := doRequest(t, "GET", fmt.Sprintf("http://127.0.0.1:%d/health", port), "", nil) if status != 200 { t.Fatalf("status = %d, want 200; body: %s", status, body) } var result map[string]interface{} json.Unmarshal([]byte(body), &result) if result["ok"] != true { t.Errorf("ok = %v, want true", result["ok"]) } if result["version"] != "1.0.0" { t.Errorf("version = %v, want 1.0.0", result["version"]) } } func TestBridgeServer_Models(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBinWithModels(t) cfg := makeTestConfig(agentBin, port) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() status, body := doRequest(t, "GET", fmt.Sprintf("http://127.0.0.1:%d/v1/models", port), "", nil) if status != 200 { t.Fatalf("status = %d, want 200; body: %s", status, body) } var result map[string]interface{} json.Unmarshal([]byte(body), &result) if result["object"] != "list" { t.Errorf("object = %v, want list", result["object"]) } data := result["data"].([]interface{}) if len(data) < 2 { t.Errorf("data len = %d, want >= 2", len(data)) } } func TestBridgeServer_Unauthorized(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBinWithModels(t) cfg := makeTestConfig(agentBin, port, func(c *config.BridgeConfig) { c.RequiredKey = "secret123" }) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() status, body := doRequest(t, "GET", fmt.Sprintf("http://127.0.0.1:%d/health", port), "", nil) if status != 401 { t.Fatalf("status = %d, want 401; body: %s", status, body) } var result map[string]interface{} json.Unmarshal([]byte(body), &result) errObj := result["error"].(map[string]interface{}) if errObj["message"] != "Invalid API key" { t.Errorf("message = %v, want 'Invalid API key'", errObj["message"]) } } func TestBridgeServer_AuthorizedKey(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBinWithModels(t) cfg := makeTestConfig(agentBin, port, func(c *config.BridgeConfig) { c.RequiredKey = "secret123" }) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() status, _ := doRequest(t, "GET", fmt.Sprintf("http://127.0.0.1:%d/health", port), "", map[string]string{ "Authorization": "Bearer secret123", }) if status != 200 { t.Errorf("status = %d, want 200", status) } } func TestBridgeServer_NotFound(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBinWithModels(t) cfg := makeTestConfig(agentBin, port) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() status, body := doRequest(t, "GET", fmt.Sprintf("http://127.0.0.1:%d/unknown", port), "", nil) if status != 404 { t.Fatalf("status = %d, want 404; body: %s", status, body) } var result map[string]interface{} json.Unmarshal([]byte(body), &result) errObj := result["error"].(map[string]interface{}) if errObj["code"] != "not_found" { t.Errorf("code = %v, want not_found", errObj["code"]) } } func TestBridgeServer_ChatCompletions_Sync(t *testing.T) { port := freePort(t) agentBin := makeFakeAgentBin(t, "Hello from agent") cfg := makeTestConfig(agentBin, port) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) waitListening(t, "127.0.0.1", port, 3*time.Second) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() reqBody := `{"model":"claude-3-opus","messages":[{"role":"user","content":"Hi"}]}` status, body := doRequest(t, "POST", fmt.Sprintf("http://127.0.0.1:%d/v1/chat/completions", port), reqBody, nil) if status != 200 { t.Fatalf("status = %d, want 200; body: %s", status, body) } var result map[string]interface{} json.Unmarshal([]byte(body), &result) if result["object"] != "chat.completion" { t.Errorf("object = %v, want chat.completion", result["object"]) } choices := result["choices"].([]interface{}) msg := choices[0].(map[string]interface{})["message"].(map[string]interface{}) if msg["content"] != "Hello from agent" { t.Errorf("content = %v, want 'Hello from agent'", msg["content"]) } } func TestBridgeServer_MultiPort(t *testing.T) { basePort := freePort(t) agentBin := makeFakeAgentBinWithModels(t) dir1 := t.TempDir() dir2 := t.TempDir() cfg := makeTestConfig(agentBin, basePort, func(c *config.BridgeConfig) { c.ConfigDirs = []string{dir1, dir2} c.MultiPort = true }) srvs := server.StartBridgeServer(server.ServerOptions{Version: "1.0.0", Config: cfg}) if len(srvs) != 2 { t.Fatalf("got %d servers, want 2", len(srvs)) } // 等待兩個 server 啟動(port 可能會衝突,這裡不嚴格測試 port 分配) time.Sleep(200 * time.Millisecond) defer func() { for _, s := range srvs { s.Shutdown(context.Background()) } }() }