返回 DeepSeek-Reasonix
web_test.go
根目录 / internal / cli / web_test.go
1 package cli
2
3 import (
4 "context"
5 "net"
6 "strings"
7 "testing"
8 "time"
9
10 "reasonix/internal/config"
11 "reasonix/internal/control"
12 "reasonix/internal/serve"
13 )
14
15 func TestRunDispatchesWebCommand(t *testing.T) {
16 isolateCLIConfigHome(t)
17 previous := runWebCommand
18 t.Cleanup(func() { runWebCommand = previous })
19
20 var got []string
21 runWebCommand = func(args []string) int {
22 got = append([]string(nil), args...)
23 return 29
24 }
25 if code := Run([]string{"web", "--addr", "127.0.0.1:0"}, "test-version"); code != 29 {
26 t.Fatalf("reasonix web exit code = %d, want 29", code)
27 }
28 if strings.Join(got, " ") != "--addr 127.0.0.1:0" {
29 t.Fatalf("reasonix web args = %q", got)
30 }
31 }
32
33 func TestHelpListsWebCommand(t *testing.T) {
34 out := captureStdout(t, func() {
35 if rc := Run([]string{"help"}, "test-version"); rc != 0 {
36 t.Fatalf("help rc = %d, want 0", rc)
37 }
38 })
39 if !strings.Contains(out, "reasonix web") {
40 t.Fatalf("help output missing web command:\n%s", out)
41 }
42 }
43
44 func TestWebBrowserURLUsesReachableLoopbackAndAuthEntry(t *testing.T) {
45 tests := []struct {
46 name string
47 cfg config.ServeConfig
48 addr string
49 want string
50 }{
51 {name: "no auth wildcard IPv4", cfg: config.ServeConfig{AuthMode: "none"}, addr: "0.0.0.0:8787", want: "http://127.0.0.1:8787/#token="},
52 {name: "no auth wildcard IPv6", cfg: config.ServeConfig{AuthMode: "none"}, addr: "[::]:8788", want: "http://127.0.0.1:8788/#token="},
53 {name: "password", cfg: config.ServeConfig{AuthMode: "password", PasswordHash: "$2a$12$test"}, addr: "127.0.0.1:8789", want: "http://127.0.0.1:8789/login"},
54 {name: "token escaped", cfg: config.ServeConfig{AuthMode: "token", Token: "a token/+"}, addr: "127.0.0.1:8790", want: "http://127.0.0.1:8790/#token=a+token%2F%2B"},
55 }
56 for _, tt := range tests {
57 t.Run(tt.name, func(t *testing.T) {
58 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
59 t.Cleanup(ctrl.Close)
60 srv := serve.New(ctrl, serve.NewBroadcaster(), tt.cfg)
61 want := tt.want
62 if tt.cfg.AuthMode == "none" {
63 want += srv.AuthToken()
64 }
65 if got := webBrowserURL(srv, tt.addr, ""); got != want {
66 t.Fatalf("webBrowserURL() = %q, want %q", got, want)
67 }
68 })
69 }
70 }
71
72 type acceptGateListener struct {
73 net.Listener
74 entered chan struct{}
75 release chan struct{}
76 }
77
78 func (l *acceptGateListener) Accept() (net.Conn, error) {
79 select {
80 case <-l.entered:
81 default:
82 close(l.entered)
83 }
84 <-l.release
85 return l.Listener.Accept()
86 }
87
88 func TestRunServeListenerOpensOnlyAfterHTTPResponse(t *testing.T) {
89 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
90 t.Cleanup(ctrl.Close)
91 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{})
92 raw, err := net.Listen("tcp", "127.0.0.1:0")
93 if err != nil {
94 t.Fatal(err)
95 }
96 ln := &acceptGateListener{Listener: raw, entered: make(chan struct{}), release: make(chan struct{})}
97 ctx, cancel := context.WithCancel(context.Background())
98 opened := make(chan struct{})
99 done := make(chan error, 1)
100 go func() {
101 done <- runServeListenerAfterReady(ctx, srv, ln, raw.Addr().String(), func() {
102 close(opened)
103 cancel()
104 })
105 }()
106
107 select {
108 case <-ln.entered:
109 case <-time.After(2 * time.Second):
110 t.Fatal("HTTP server never reached Accept")
111 }
112 select {
113 case <-opened:
114 t.Fatal("browser callback ran before the HTTP server could respond")
115 default:
116 }
117 close(ln.release)
118 select {
119 case <-opened:
120 case <-time.After(2 * time.Second):
121 t.Fatal("browser callback did not run after the HTTP server responded")
122 }
123 select {
124 case err := <-done:
125 if err != nil {
126 t.Fatalf("runServeListenerAfterReady() error = %v", err)
127 }
128 case <-time.After(2 * time.Second):
129 t.Fatal("server did not stop after callback canceled its context")
130 }
131 }
132
133 func TestRunWebHelpDoesNotOpenBrowser(t *testing.T) {
134 previous := openBrowserURL
135 t.Cleanup(func() { openBrowserURL = previous })
136 openBrowserURL = func(string) error {
137 t.Fatal("reasonix web --help must not open a browser")
138 return nil
139 }
140 if code := runWeb([]string{"--help"}); code != 0 {
141 t.Fatalf("runWeb(--help) = %d, want 0", code)
142 }
143 }
144
145 func TestLaunchWebBrowserInvokesOpenerWithResolvedURL(t *testing.T) {
146 previous := openBrowserURL
147 t.Cleanup(func() { openBrowserURL = previous })
148 var opened string
149 openBrowserURL = func(raw string) error {
150 opened = raw
151 return nil
152 }
153 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
154 t.Cleanup(ctrl.Close)
155 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{})
156 got, err := launchWebBrowser(srv, "0.0.0.0:8787", "")
157 if err != nil {
158 t.Fatal(err)
159 }
160 if got != "http://127.0.0.1:8787/#token="+srv.AuthToken() || opened != got {
161 t.Fatalf("launchWebBrowser() = %q, opened %q", got, opened)
162 }
163 }
164
165 func TestWebHandoffArgsResumeSameSessionOnDeterministicPort(t *testing.T) {
166 got := webHandoffArgs("/tmp/session.jsonl", "session", "deepseek/deepseek-v4-flash")
167 want := "--resume /tmp/session.jsonl --model deepseek/deepseek-v4-flash"
168 if strings.Join(got, " ") != want {
169 t.Fatalf("webHandoffArgs() = %q, want %q", strings.Join(got, " "), want)
170 }
171 }
172
173 func TestWebBrowserURLUsesSessionDeepLinkBeforeTokenFragment(t *testing.T) {
174 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
175 t.Cleanup(ctrl.Close)
176 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{AuthMode: "token", Token: "secret"})
177 got := webBrowserURL(srv, "127.0.0.1:8787", "session with space")
178 want := "http://127.0.0.1:8787/sessions/session%20with%20space#token=secret"
179 if got != want {
180 t.Fatalf("webBrowserURL() = %q, want %q", got, want)
181 }
182 }
183
183 lines GO