返回 DeepSeek-Reasonix
session_takeover_close_test.go
根目录 / desktop / session_takeover_close_test.go
1 package main
2
3 import (
4 "net/http"
5 "net/http/httptest"
6 "path/filepath"
7 "sync/atomic"
8 "testing"
9 "time"
10 )
11
12 // closeMirrorFixture registers a mirror for sessionPath against a stub Serve
13 // that records when the farewell arrives.
14 func closeMirrorFixture(t *testing.T, app *App, sessionPath string) (*takeoverMirror, *atomic.Int32) {
15 t.Helper()
16 var ends atomic.Int32
17 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 if r.URL.Path == "/mirror-end" {
19 ends.Add(1)
20 w.WriteHeader(http.StatusNoContent)
21 return
22 }
23 w.WriteHeader(http.StatusNotFound)
24 }))
25 t.Cleanup(srv.Close)
26 m := &takeoverMirror{
27 app: app, key: sessionRuntimeKey(sessionPath), sessionPath: sessionPath,
28 client: srv.Client(), record: takeoverServeRecord{base: srv.URL},
29 grant: takeoverGrant{MirrorID: "mirror"},
30 wake: make(chan struct{}, 1),
31 }
32 app.takeoverMu.Lock()
33 if app.takeoverMirrors == nil {
34 app.takeoverMirrors = map[string]*takeoverMirror{}
35 }
36 app.takeoverMirrors[m.key] = m
37 app.takeoverMu.Unlock()
38 return m, &ends
39 }
40
41 // Serve waits for the writer to drop before reclaiming a mirrored session, so
42 // the farewell must follow the writer release rather than race it. A closing
43 // tab claims the farewell while it still owns the writer and sends it after
44 // the release; the mirror loop's tab-gone branch must not pre-empt that.
45 func TestClosingTabOwnsTheMirrorFarewellUntilTheWriterIsReleased(t *testing.T) {
46 app := NewApp()
47 path := filepath.Join(t.TempDir(), "closing.jsonl")
48 m, ends := closeMirrorFixture(t, app, path)
49
50 claimed, release := app.claimTakeoverMirrorFarewell(path)
51 if claimed != m || !m.closing.Load() {
52 t.Fatalf("claim = %p, want the registered mirror marked closing", claimed)
53 }
54 defer release()
55 // While the close is still tearing the writer down, the loop's tab-gone
56 // branch only detaches: announcing the writer as gone here is exactly the
57 // ordering that makes Serve wait.
58 m.detach()
59 if !m.closing.Load() {
60 t.Fatal("detach cleared the close claim")
61 }
62 if ends.Load() != 0 {
63 t.Fatalf("farewell sent %d times before the writer was released", ends.Load())
64 }
65
66 app.endTakeoverMirrorForClosedTab(m)
67 deadline := time.Now().Add(20 * time.Second)
68 for ends.Load() == 0 {
69 if time.Now().After(deadline) {
70 t.Fatal("the close epilogue never sent the farewell")
71 }
72 time.Sleep(time.Millisecond)
73 }
74
75 // A delivered farewell is not repeated: the loop's backstop and the close
76 // epilogue must not both announce the writer as gone.
77 m.mirrorEnd()
78 if got := ends.Load(); got != 1 {
79 t.Fatalf("farewell sent %d times, want exactly one", got)
80 }
81 }
82
83 // An undelivered farewell stays unmarked so a later path can retry it.
84 func TestUndeliveredMirrorFarewellStaysRetryable(t *testing.T) {
85 app := NewApp()
86 path := filepath.Join(t.TempDir(), "unreachable.jsonl")
87 m, _ := closeMirrorFixture(t, app, path)
88 m.mu.Lock()
89 m.record = takeoverServeRecord{base: "http://127.0.0.1:1"}
90 m.mu.Unlock()
91
92 m.mirrorEnd()
93 if m.ended.Load() {
94 t.Fatal("a failed farewell was recorded as delivered")
95 }
96 }
97
98 // A close that returns early keeps its writer, so the farewell claim goes back
99 // to the mirror loop instead of stranding the mirror silent.
100 func TestAbandonedCloseReturnsTheFarewellClaim(t *testing.T) {
101 app := NewApp()
102 path := filepath.Join(t.TempDir(), "abandoned.jsonl")
103 m, ends := closeMirrorFixture(t, app, path)
104
105 func() {
106 _, release := app.claimTakeoverMirrorFarewell(path)
107 defer release()
108 // The close bails out here (detached runtime, or the tab changed).
109 }()
110
111 if m.closing.Load() {
112 t.Fatal("an abandoned close kept the farewell claim")
113 }
114 m.mirrorEnd()
115 if ends.Load() != 1 {
116 t.Fatalf("farewell sent %d times after the claim was returned, want one", ends.Load())
117 }
118 }
119
119 lines GO