返回 DeepSeek-Reasonix
publish_test.go
根目录 / internal / extension / publish_test.go
1 package extension
2
3 import (
4 "testing"
5 "time"
6 )
7
8 func TestPublishGateStaleAndAdmit(t *testing.T) {
9 g := NewPublishGate()
10 g.Publish(2)
11 if g.Published() != 2 {
12 t.Fatalf("published = %d", g.Published())
13 }
14 // Only older than published is stale; equal is current.
15 if !g.IsStale(1) || g.IsStale(2) || g.IsStale(0) || g.IsStale(3) {
16 t.Fatal("stale checks failed")
17 }
18 if !g.AdmitNewWork(2) || g.AdmitNewWork(1) {
19 t.Fatal("admit checks failed")
20 }
21 if got := g.DrainingGenerations(); len(got) != 0 {
22 t.Fatalf("first publish unexpectedly drained generations: %v", got)
23 }
24 g.Publish(3)
25 if !g.IsDraining(2) {
26 t.Fatal("gen 2 should be draining")
27 }
28 if g.DropStale(2, "ui") != true {
29 t.Fatal("drop stale")
30 }
31 }
32
33 func TestPublishGateSweep(t *testing.T) {
34 g := NewPublishGate().WithDrainTTL(time.Millisecond)
35 g.Publish(1)
36 g.Publish(2)
37 g.mu.Lock()
38 g.draining[1] = time.Now().Add(-time.Second)
39 g.mu.Unlock()
40 expired := g.SweepExpiredDrains()
41 if len(expired) != 1 || expired[0] != 1 {
42 t.Fatalf("expired = %v", expired)
43 }
44 }
45
46 func TestPublishGateSweepAndForceExpireRecordsReceipt(t *testing.T) {
47 // Isolate default store pollution by using a private gate + checking store
48 // has a drain-timeout receipt for the expired gen.
49 g := NewPublishGate().WithDrainTTL(time.Millisecond)
50 g.Publish(10)
51 g.Publish(11)
52 g.mu.Lock()
53 g.draining[10] = time.Now().Add(-time.Second)
54 g.mu.Unlock()
55 expired := g.SweepAndForceExpire()
56 if len(expired) != 1 || expired[0] != 10 {
57 t.Fatalf("expired = %v", expired)
58 }
59 if _, ok := g.receipts.Get("drain-timeout-10"); !ok {
60 t.Fatal("expected drain-timeout receipt")
61 }
62 if g.IsDraining(10) {
63 t.Fatal("gen 10 should no longer be draining")
64 }
65 }
66
67 func TestPublishGateLateDrainCancelFiresImmediately(t *testing.T) {
68 g := NewPublishGate()
69 g.Publish(20)
70 g.Publish(21)
71 g.ForceExpireDrain(20)
72
73 fired := false
74 g.RegisterDrainCancel(20, func() { fired = true })
75 if !fired {
76 t.Fatal("cancel registered after force-expire must fire immediately")
77 }
78 }
79
80 func TestPublishGateDrainCancelCanBeUnregistered(t *testing.T) {
81 g := NewPublishGate()
82 g.Publish(30)
83 fired := false
84 unregister := g.RegisterDrainCancel(30, func() { fired = true })
85 unregister()
86 unregister()
87
88 g.mu.RLock()
89 pending := len(g.drainCancels[30])
90 g.mu.RUnlock()
91 if pending != 0 {
92 t.Fatalf("pending drain cancels = %d, want 0", pending)
93 }
94 g.ForceExpireDrain(30)
95 if fired {
96 t.Fatal("unregistered drain cancel fired")
97 }
98 }
99
100 func TestPublishGateDrainWatchSkipsColdPublishAndCoalesces(t *testing.T) {
101 g := NewPublishGate().WithDrainTTL(time.Hour)
102 g.Publish(1)
103 g.ScheduleDrainWatch()
104 g.mu.RLock()
105 coldWatching := g.drainWatching
106 g.mu.RUnlock()
107 if coldWatching {
108 t.Fatal("cold publish without a draining generation started a watcher")
109 }
110
111 g.Publish(2)
112 g.ScheduleDrainWatch()
113 g.ScheduleDrainWatch()
114 g.mu.RLock()
115 watching := g.drainWatching
116 g.mu.RUnlock()
117 if !watching {
118 t.Fatal("active drain did not start its coalesced watcher")
119 }
120 }
121
122 func TestPublishGateBoundsExpiredGenerations(t *testing.T) {
123 g := NewPublishGate()
124 g.expiredLimit = 2
125 for gen := uint64(1); gen <= 3; gen++ {
126 g.ForceExpireDrain(gen)
127 }
128 g.mu.RLock()
129 expiredLen := len(g.expired)
130 orderLen := len(g.expiredOrder)
131 _, hasFirst := g.expired[1]
132 _, hasSecond := g.expired[2]
133 _, hasThird := g.expired[3]
134 g.mu.RUnlock()
135 if expiredLen != 2 || orderLen != 2 {
136 t.Fatalf("expired retention = map:%d order:%d, want 2", expiredLen, orderLen)
137 }
138 if hasFirst {
139 t.Fatal("oldest expired generation was not evicted")
140 }
141 if !hasSecond || !hasThird {
142 t.Fatalf("latest expired generations retained = second:%v third:%v, want true/true", hasSecond, hasThird)
143 }
144
145 g.Publish(4)
146 fired := false
147 g.RegisterDrainCancel(1, func() { fired = true })
148 if !fired {
149 t.Fatal("late cancel for an evicted expired generation was retained")
150 }
151 }
152
153 func TestLifecycleTransitions(t *testing.T) {
154 r := NewLifecycleRegistry(5)
155 r.Ensure("plugin/a")
156 if err := r.Transition("plugin/a", ComponentPreparing, ""); err != nil {
157 t.Fatal(err)
158 }
159 if err := r.Transition("plugin/a", ComponentActive, ""); err != nil {
160 t.Fatal(err)
161 }
162 if err := r.Transition("plugin/a", ComponentInactive, ""); err == nil {
163 t.Fatal("Active -> Inactive without Draining should fail")
164 }
165 if err := r.Transition("plugin/a", ComponentDraining, ""); err != nil {
166 t.Fatal(err)
167 }
168 if err := r.Transition("plugin/a", ComponentInactive, ""); err != nil {
169 t.Fatal(err)
170 }
171 st, ok := r.Status("plugin/a")
172 if !ok || st.State != ComponentInactive {
173 t.Fatalf("status = %+v", st)
174 }
175 }
176
176 lines GO