返回 DeepSeek-Reasonix
serve_provider_setup_test.go
根目录 / internal / cli / serve_provider_setup_test.go
1 package cli
2
3 import (
4 "bytes"
5 "io"
6 "net/http"
7 "net/http/cookiejar"
8 "net/http/httptest"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15 )
16
17 const serveMissingKeyHelperEnv = "REASONIX_TEST_SERVE_MISSING_KEY_HELPER"
18
19 // TestServeStartsWithMissingProviderKey exercises the real runServe lifecycle:
20 // the listener and port file must become available before a Provider key exists,
21 // and the authenticated browser surface must show the setup page.
22 func TestServeStartsWithMissingProviderKey(t *testing.T) {
23 if os.Getenv(serveMissingKeyHelperEnv) == "1" {
24 // The package TestMain clears path overrides before dispatching tests, so
25 // restore this helper's isolated home after that process-wide guard runs.
26 if err := os.Setenv("REASONIX_HOME", os.Getenv("REASONIX_TEST_SERVE_HOME")); err != nil {
27 os.Exit(2)
28 }
29 code := runServe([]string{
30 "--model", "remote-demo/model-a",
31 "--addr", "127.0.0.1:0",
32 "--port-file", os.Getenv("REASONIX_TEST_SERVE_PORT_FILE"),
33 "--auth", "token",
34 "--token", "serve-setup-test-token",
35 })
36 os.Exit(code)
37 }
38
39 home := t.TempDir()
40 balanceStarted := make(chan struct{})
41 releaseBalance := make(chan struct{})
42 balanceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
43 select {
44 case <-balanceStarted:
45 default:
46 close(balanceStarted)
47 }
48 <-releaseBalance
49 w.Header().Set("Content-Type", "application/json")
50 _, _ = w.Write([]byte(`{}`))
51 }))
52 t.Cleanup(balanceServer.Close)
53 t.Cleanup(func() { close(releaseBalance) })
54 configPath := filepath.Join(home, "config.toml")
55 configBody := `default_model = "remote-demo/model-a"
56
57 [[providers]]
58 name = "remote-demo"
59 kind = "openai"
60 base_url = "https://example.invalid/v1"
61 balance_url = "` + balanceServer.URL + `"
62 models = ["model-a"]
63 default = "model-a"
64 api_key_env = "REASONIX_TEST_REMOTE_MISSING_KEY"
65 `
66 if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
67 t.Fatal(err)
68 }
69 portFile := filepath.Join(home, "serve.addr")
70 cmd := exec.Command(os.Args[0], "-test.run=^TestServeStartsWithMissingProviderKey$")
71 cmd.Env = replaceServeTestEnv(os.Environ(),
72 serveMissingKeyHelperEnv+"=1",
73 "REASONIX_HOME="+home,
74 "REASONIX_TEST_SERVE_HOME="+home,
75 "REASONIX_CREDENTIALS_STORE=file",
76 "REASONIX_TEST_REMOTE_MISSING_KEY=",
77 "REASONIX_TEST_SERVE_PORT_FILE="+portFile,
78 )
79 var output bytes.Buffer
80 cmd.Stdout = &output
81 cmd.Stderr = &output
82 if err := cmd.Start(); err != nil {
83 t.Fatal(err)
84 }
85 t.Cleanup(func() {
86 if cmd.Process != nil {
87 _ = cmd.Process.Kill()
88 _ = cmd.Wait()
89 }
90 })
91
92 var addr string
93 var lastReadErr error
94 deadline := time.Now().Add(15 * time.Second)
95 for time.Now().Before(deadline) {
96 data, err := os.ReadFile(portFile)
97 if err == nil && strings.TrimSpace(string(data)) != "" {
98 addr = strings.TrimSpace(string(data))
99 break
100 }
101 // Every read error is retryable inside the deadline: on Windows the
102 // writing child briefly holds the file (sharing violation), which is a
103 // timing condition, not a failure.
104 lastReadErr = err
105 time.Sleep(20 * time.Millisecond)
106 }
107 if addr == "" {
108 t.Fatalf("Serve did not publish its port with a missing Provider key (last read err: %v):\n%s", lastReadErr, output.String())
109 }
110 select {
111 case <-balanceStarted:
112 case <-time.After(5 * time.Second):
113 t.Fatalf("Serve did not start the configured balance diagnostic:\n%s", output.String())
114 }
115
116 jar, err := cookiejar.New(nil)
117 if err != nil {
118 t.Fatal(err)
119 }
120 client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
121 resp, err := client.Get("http://" + addr + "/?token=serve-setup-test-token")
122 if err != nil {
123 t.Fatalf("open missing-key Serve: %v\n%s", err, output.String())
124 }
125 body, err := io.ReadAll(resp.Body)
126 resp.Body.Close()
127 if err != nil {
128 t.Fatal(err)
129 }
130 if resp.StatusCode != http.StatusOK {
131 t.Fatalf("missing-key Serve status = %d, want 200: %s\n%s", resp.StatusCode, body, output.String())
132 }
133 if !bytes.Contains(body, []byte("Reasonix Provider Setup")) {
134 t.Fatalf("missing-key Serve did not show setup page:\n%s", body)
135 }
136 }
137
138 func replaceServeTestEnv(base []string, overrides ...string) []string {
139 keys := make(map[string]bool, len(overrides))
140 for _, item := range overrides {
141 key, _, _ := strings.Cut(item, "=")
142 keys[key] = true
143 }
144 out := make([]string, 0, len(base)+len(overrides))
145 for _, item := range base {
146 key, _, _ := strings.Cut(item, "=")
147 if !keys[key] {
148 out = append(out, item)
149 }
150 }
151 return append(out, overrides...)
152 }
153
153 lines GO