返回 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", addr: "0.0.0.0:8787", want: "http://127.0.0.1:8787/"},
52 {name: "no auth wildcard IPv6", addr: "[::]:8788", want: "http://127.0.0.1:8788/"},
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 if got := webBrowserURL(srv, tt.addr, ""); got != tt.want {
62 t.Fatalf("webBrowserURL() = %q, want %q", got, tt.want)
63 }
64 })
65 }
66 }
67
68 type acceptGateListener struct {
69 net.Listener
70 entered chan struct{}
71 release chan struct{}
72 }
73
74 func (l *acceptGateListener) Accept() (net.Conn, error) {
75 select {
76 case <-l.entered:
77 default:
78 close(l.entered)
79 }
80 <-l.release
81 return l.Listener.Accept()
82 }
83
84 func TestRunServeListenerOpensOnlyAfterHTTPResponse(t *testing.T) {
85 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
86 t.Cleanup(ctrl.Close)
87 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{})
88 raw, err := net.Listen("tcp", "127.0.0.1:0")
89 if err != nil {
90 t.Fatal(err)
91 }
92 ln := &acceptGateListener{Listener: raw, entered: make(chan struct{}), release: make(chan struct{})}
93 ctx, cancel := context.WithCancel(context.Background())
94 opened := make(chan struct{})
95 done := make(chan error, 1)
96 go func() {
97 done <- runServeListenerAfterReady(ctx, srv, ln, raw.Addr().String(), func() {
98 close(opened)
99 cancel()
100 })
101 }()
102
103 select {
104 case <-ln.entered:
105 case <-time.After(2 * time.Second):
106 t.Fatal("HTTP server never reached Accept")
107 }
108 select {
109 case <-opened:
110 t.Fatal("browser callback ran before the HTTP server could respond")
111 default:
112 }
113 close(ln.release)
114 select {
115 case <-opened:
116 case <-time.After(2 * time.Second):
117 t.Fatal("browser callback did not run after the HTTP server responded")
118 }
119 select {
120 case err := <-done:
121 if err != nil {
122 t.Fatalf("runServeListenerAfterReady() error = %v", err)
123 }
124 case <-time.After(2 * time.Second):
125 t.Fatal("server did not stop after callback canceled its context")
126 }
127 }
128
129 func TestRunWebHelpDoesNotOpenBrowser(t *testing.T) {
130 previous := openBrowserURL
131 t.Cleanup(func() { openBrowserURL = previous })
132 openBrowserURL = func(string) error {
133 t.Fatal("reasonix web --help must not open a browser")
134 return nil
135 }
136 if code := runWeb([]string{"--help"}); code != 0 {
137 t.Fatalf("runWeb(--help) = %d, want 0", code)
138 }
139 }
140
141 func TestLaunchWebBrowserInvokesOpenerWithResolvedURL(t *testing.T) {
142 previous := openBrowserURL
143 t.Cleanup(func() { openBrowserURL = previous })
144 var opened string
145 openBrowserURL = func(raw string) error {
146 opened = raw
147 return nil
148 }
149 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
150 t.Cleanup(ctrl.Close)
151 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{})
152 got, err := launchWebBrowser(srv, "0.0.0.0:8787", "")
153 if err != nil {
154 t.Fatal(err)
155 }
156 if got != "http://127.0.0.1:8787/" || opened != got {
157 t.Fatalf("launchWebBrowser() = %q, opened %q", got, opened)
158 }
159 }
160
161 func TestWebHandoffArgsResumeSameSessionOnDeterministicPort(t *testing.T) {
162 got := webHandoffArgs("/tmp/session.jsonl", "session", "deepseek/deepseek-v4-flash")
163 want := "--resume /tmp/session.jsonl --model deepseek/deepseek-v4-flash"
164 if strings.Join(got, " ") != want {
165 t.Fatalf("webHandoffArgs() = %q, want %q", strings.Join(got, " "), want)
166 }
167 }
168
169 func TestWebBrowserURLUsesSessionDeepLinkBeforeTokenFragment(t *testing.T) {
170 ctrl := newOwnedTestController(t, control.Options{SessionDir: t.TempDir()})
171 t.Cleanup(ctrl.Close)
172 srv := serve.New(ctrl, serve.NewBroadcaster(), config.ServeConfig{AuthMode: "token", Token: "secret"})
173 got := webBrowserURL(srv, "127.0.0.1:8787", "session with space")
174 want := "http://127.0.0.1:8787/sessions/session%20with%20space#token=secret"
175 if got != want {
176 t.Fatalf("webBrowserURL() = %q, want %q", got, want)
177 }
178 }
179
179 lines GO