返回 DeepSeek-Reasonix
authentication_app_test.go
根目录 / desktop / authentication_app_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "reasonix/internal/control"
11 )
12
13 type desktopAuthenticationRunner struct{ calls atomic.Int32 }
14
15 func (r *desktopAuthenticationRunner) Run(context.Context, string) error {
16 r.calls.Add(1)
17 return nil
18 }
19
20 func newDesktopAuthenticationTab(t *testing.T, app *App, id string, state control.AuthenticationState) (*WorkspaceTab, *control.Controller, *desktopAuthenticationRunner) {
21 t.Helper()
22 runner := &desktopAuthenticationRunner{}
23 ctrl := control.New(control.Options{Runner: runner, Authentication: state})
24 t.Cleanup(ctrl.Close)
25 tab := &WorkspaceTab{ID: id, Ctrl: ctrl, Ready: true, Scope: "global"}
26 app.tabs[id] = tab
27 return tab, ctrl, runner
28 }
29
30 func TestDesktopAuthenticationAdmissionDoesNotCreateTurn(t *testing.T) {
31 app := NewApp()
32 tab, ctrl, runner := newDesktopAuthenticationTab(t, app, "missing", control.AuthenticationState{
33 Status: control.AuthenticationMissingCredential, ProviderName: "relay", ModelRef: "relay/chat",
34 })
35 app.activeTabID = tab.ID
36
37 err := app.SubmitToTab(tab.ID, "must remain a draft")
38 var authErr *control.AuthenticationError
39 if !errors.As(err, &authErr) || authErr.State.Status != control.AuthenticationMissingCredential {
40 t.Fatalf("SubmitToTab error = %v, want missing-credential AuthenticationError", err)
41 }
42 if runner.calls.Load() != 0 || ctrl.Turn() != 0 || len(ctrl.History()) != 0 {
43 t.Fatalf("blocked submit changed runtime: calls=%d turn=%d history=%d", runner.calls.Load(), ctrl.Turn(), len(ctrl.History()))
44 }
45 }
46
47 func TestRetryAuthenticationForTabIsSingleUseAndTabLocal(t *testing.T) {
48 app := NewApp()
49 first, firstCtrl, _ := newDesktopAuthenticationTab(t, app, "first", control.AuthenticationState{
50 Status: control.AuthenticationRejected, ProviderName: "first-provider", HTTPStatus: 401,
51 })
52 second, secondCtrl, _ := newDesktopAuthenticationTab(t, app, "second", control.AuthenticationState{
53 Status: control.AuthenticationRejected, ProviderName: "second-provider", HTTPStatus: 403,
54 })
55 app.activeTabID = second.ID
56
57 state, err := app.RetryAuthenticationForTab(first.ID)
58 if err != nil || !state.Ready() {
59 t.Fatalf("first retry = %+v, %v", state, err)
60 }
61 if _, err := app.RetryAuthenticationForTab(first.ID); err == nil {
62 t.Fatal("second retry was accepted without another rejection")
63 }
64 if got := firstCtrl.AuthenticationState(); !got.Ready() {
65 t.Fatalf("first tab state = %+v, want ready for one attempt", got)
66 }
67 if got := secondCtrl.AuthenticationState(); got.Status != control.AuthenticationRejected || got.ProviderName != "second-provider" {
68 t.Fatalf("retry leaked into second tab: %+v", got)
69 }
70 }
71
72 func TestRetryAuthenticationRejectsMissingCredential(t *testing.T) {
73 app := NewApp()
74 tab, _, _ := newDesktopAuthenticationTab(t, app, "missing", control.AuthenticationState{Status: control.AuthenticationMissingCredential})
75 if _, err := app.RetryAuthenticationForTab(tab.ID); err == nil {
76 t.Fatal("missing credential incorrectly accepted authentication retry")
77 }
78 }
79
80 func TestRetryAuthenticationPublishesAuthoritativeTabRefresh(t *testing.T) {
81 app := NewApp()
82 app.ctx = context.Background()
83 tab, _, _ := newDesktopAuthenticationTab(t, app, "retry-refresh", control.AuthenticationState{Status: control.AuthenticationRejected, ProviderName: "relay", HTTPStatus: 401})
84 app.activeTabID = tab.ID
85 installNoopRuntimeEvents(app)
86 refreshed := make(chan bool, 1)
87 app.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) {
88 if name != tabMetaRefreshEventChannel {
89 return
90 }
91 for _, item := range app.ListTabs() {
92 if item.ID == tab.ID && item.Authentication != nil && item.Authentication.Ready() {
93 // 非阻塞发送:该回调跑在共享的 asyncRuntimeEmitter.run 上,
94 // 阻塞发送会把后续所有 runtime 事件永久卡死。
95 select {
96 case refreshed <- true:
97 default:
98 }
99 return
100 }
101 }
102 }
103 if _, err := app.RetryAuthenticationForTab(tab.ID); err != nil {
104 t.Fatal(err)
105 }
106 select {
107 case <-refreshed:
108 case <-time.After(5 * time.Second):
109 t.Fatal("retry did not publish a refresh of authoritative authentication state")
110 }
111
112 // 回归:缓冲已满时再次触发同一事件,emitter 泵必须继续排空而不是停在
113 // tabs.go 的 emit 调用点上(曾实证 chan send 卡死 7 分钟)。
114 for range 3 {
115 app.runtimeEvents.Emit(app.ctx, tabMetaRefreshEventChannel)
116 }
117 deadline := time.Now().Add(5 * time.Second)
118 for {
119 app.runtimeEvents.mu.Lock()
120 busy := app.runtimeEvents.running
121 app.runtimeEvents.mu.Unlock()
122 if !busy {
123 return
124 }
125 if time.Now().After(deadline) {
126 t.Fatal("asyncRuntimeEmitter pump stayed blocked after repeated refresh events")
127 }
128 time.Sleep(10 * time.Millisecond)
129 }
130 }
131
131 lines GO