返回 DeepSeek-Reasonix
cred_proxy_smoke_test.go
根目录 / desktop / cred_proxy_smoke_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/cookiejar"
10 "os"
11 "os/user"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16 )
17
18 // TestCredentialProxyRealHostSmoke is a manual E2E against a real SSH host:
19 // set CRED_PROXY_SMOKE_HOST=<hostID> (whose config entry has
20 // credential_mode = "local-proxy") to run. It drives the full desktop path —
21 // connect, reverse tunnel, credential proxy, serve launch with the virtual
22 // token — then submits one real model turn and asserts the reply arrived
23 // through desktop-held credentials. Costs one tiny provider call.
24 func TestCredentialProxyRealHostSmoke(t *testing.T) {
25 hostID := os.Getenv("CRED_PROXY_SMOKE_HOST")
26 if hostID == "" {
27 t.Skip("set CRED_PROXY_SMOKE_HOST=<hostID with credential_mode=local-proxy> to run the real-host smoke")
28 }
29 // The desktop test binary's TestMain redirects HOME to a scratch dir; the
30 // smoke needs the real user config (hosts, provider, .env key). Point the
31 // config root back at the real ~/.reasonix for this test only.
32 if real, err := user.Current(); err == nil && real.HomeDir != "" {
33 t.Setenv("HOME", real.HomeDir)
34 t.Setenv("REASONIX_HOME", filepath.Join(real.HomeDir, ".reasonix"))
35 } else {
36 t.Fatal("cannot resolve the real home directory")
37 }
38 workspace := os.Getenv("CRED_PROXY_SMOKE_WORKSPACE")
39 if workspace == "" {
40 workspace = "/root/smoke-a"
41 }
42
43 a := &App{ctx: context.Background()}
44 mgr := newDesktopRemoteManager(a)
45 a.remoteRuntime = mgr
46 t.Cleanup(func() { _ = mgr.Close(); a.closeCredentialProxy() })
47
48 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
49 defer cancel()
50
51 if err := mgr.Connect(hostID); err != nil {
52 t.Fatalf("connect: %v", err)
53 }
54 if err := waitForRemoteHostSmoke(mgr, hostID, 60*time.Second); err != nil {
55 t.Fatalf("host never connected: %v", err)
56 }
57
58 view, token, err := mgr.EnsureServer(ctx, hostID, workspace)
59 if err != nil {
60 t.Fatalf("EnsureServer: %v", err)
61 }
62 if view.State != "ready" || view.LocalURL == "" {
63 t.Fatalf("serve view = %+v", view)
64 }
65 base := strings.TrimRight(view.LocalURL, "/")
66 t.Logf("serve ready at %s (workspace %s)", base, workspace)
67
68 // Verify the reverse tunnel is registered and the remote provider entry
69 // points at it.
70 foundForward := false
71 for _, f := range mgr.client(hostID).Forwards().List() {
72 if f.Spec.Name == "cred-proxy:"+hostID && f.Up {
73 foundForward = true
74 }
75 }
76 if !foundForward {
77 t.Fatal("credential proxy reverse forward is not up")
78 }
79
80 // One real model turn through the desktop-held key.
81 jar, _ := cookiejar.New(nil)
82 client := &http.Client{Jar: jar, Timeout: 120 * time.Second}
83 if err := serveHandshake(ctx, client, base, token); err != nil {
84 t.Fatalf("handshake: %v", err)
85 }
86 post := func(path string, body any) (int, []byte) {
87 data, _ := json.Marshal(body)
88 req, _ := http.NewRequest(http.MethodPost, base+path, strings.NewReader(string(data)))
89 req.Header.Set("Content-Type", "application/json")
90 resp, err := client.Do(req)
91 if err != nil {
92 t.Fatalf("%s: %v", path, err)
93 }
94 defer resp.Body.Close()
95 out, _ := io.ReadAll(resp.Body)
96 return resp.StatusCode, out
97 }
98 if code, body := post("/new", map[string]string{}); code != 204 {
99 t.Fatalf("/new: %d %s", code, body)
100 }
101 if code, body := post("/submit", map[string]string{"input": "Reply with exactly the word PROXY-OK and nothing else."}); code != 202 {
102 t.Fatalf("/submit: %d %s", code, body)
103 }
104
105 // Poll history for the assistant reply (the turn rides the tunnel).
106 deadline := time.Now().Add(90 * time.Second)
107 for {
108 resp, err := client.Get(base + "/history")
109 if err == nil {
110 out, _ := io.ReadAll(resp.Body)
111 resp.Body.Close()
112 if strings.Contains(string(out), "PROXY-OK") {
113 t.Logf("model replied through the desktop credential proxy")
114 return
115 }
116 if resp.StatusCode != http.StatusOK {
117 t.Fatalf("/history: %d %s", resp.StatusCode, out)
118 }
119 }
120 if time.Now().After(deadline) {
121 t.Fatalf("no PROXY-OK reply within 90s (last history fetch err=%v)", err)
122 }
123 time.Sleep(2 * time.Second)
124 }
125 }
126
127 func waitForRemoteHostSmoke(rt remoteKernel, hostID string, timeout time.Duration) error {
128 deadline := time.Now().Add(timeout)
129 for {
130 for _, status := range rt.Statuses() {
131 if status.HostID != hostID {
132 continue
133 }
134 switch status.State {
135 case "connected", "degraded":
136 return nil
137 case "stopped":
138 if status.Error != "" {
139 return fmt.Errorf("remote host %q: %s", hostID, status.Error)
140 }
141 return fmt.Errorf("remote host %q stopped", hostID)
142 }
143 }
144 if time.Now().After(deadline) {
145 return fmt.Errorf("remote host %q: connection timed out", hostID)
146 }
147 time.Sleep(250 * time.Millisecond)
148 }
149 }
150
150 lines GO