Skip to content

Commit 2b1d7f3

Browse files
ernadoclaude
andcommitted
test: cover pool lifecycle (managed, reap, GC, Kill, Close, acquire)
Exercise the pool's bot-management logic without a network by injecting managed entries: ready latching, idle detection, reaping, RunGC, Kill, Close and acquire dedup. Lifts pool from ~18% to ~57%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 24d81c6 commit 2b1d7f3

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

pool/pool_internal_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package pool
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
)
9+
10+
// fakeManaged returns a managed entry that is already "ready", with a cancel
11+
// hook recording whether it was killed.
12+
func fakeManaged(killed *bool) *managed {
13+
m := &managed{ready: make(chan struct{})}
14+
m.cancel = func() { *killed = true }
15+
m.markReady(nil)
16+
return m
17+
}
18+
19+
func TestManagedMarkReadyLatches(t *testing.T) {
20+
m := &managed{ready: make(chan struct{})}
21+
first := errors.New("first")
22+
m.markReady(first)
23+
m.markReady(errors.New("second")) // ignored
24+
select {
25+
case <-m.ready:
26+
default:
27+
t.Fatal("ready not closed")
28+
}
29+
if m.startErr != first {
30+
t.Fatalf("startErr = %v, want first", m.startErr)
31+
}
32+
}
33+
34+
func TestManagedIdleBefore(t *testing.T) {
35+
m := &managed{ready: make(chan struct{})}
36+
// Never used: not idle.
37+
if m.idleBefore(time.Now()) {
38+
t.Fatal("unused bot should not be idle")
39+
}
40+
m.use()
41+
if m.idleBefore(time.Now().Add(-time.Hour)) {
42+
t.Fatal("recently used bot should not be idle before an old deadline")
43+
}
44+
if !m.idleBefore(time.Now().Add(time.Hour)) {
45+
t.Fatal("bot should be idle before a future deadline")
46+
}
47+
}
48+
49+
func TestKillRemovesAndCancels(t *testing.T) {
50+
p, _ := New(Options{AppID: 1, AppHash: "x"})
51+
killed := false
52+
p.bots["123:abc"] = fakeManaged(&killed)
53+
54+
p.Kill("123:abc")
55+
if !killed {
56+
t.Fatal("Kill should cancel the bot")
57+
}
58+
if _, ok := p.bots["123:abc"]; ok {
59+
t.Fatal("Kill should remove the bot")
60+
}
61+
// Killing an unknown token is a no-op.
62+
p.Kill("nope")
63+
}
64+
65+
func TestCloseKillsAll(t *testing.T) {
66+
p, _ := New(Options{AppID: 1, AppHash: "x"})
67+
k1, k2 := false, false
68+
p.bots["1:a"] = fakeManaged(&k1)
69+
p.bots["2:b"] = fakeManaged(&k2)
70+
71+
p.Close()
72+
if !k1 || !k2 {
73+
t.Fatalf("Close should kill all: %v %v", k1, k2)
74+
}
75+
if len(p.bots) != 0 {
76+
t.Fatalf("pool not emptied: %d", len(p.bots))
77+
}
78+
}
79+
80+
func TestReapCollectsIdle(t *testing.T) {
81+
p, _ := New(Options{AppID: 1, AppHash: "x"})
82+
idleKilled, freshKilled := false, false
83+
84+
idle := fakeManaged(&idleKilled)
85+
idle.lastUsed = time.Now().Add(-time.Hour)
86+
p.bots["idle"] = idle
87+
88+
fresh := fakeManaged(&freshKilled)
89+
fresh.use()
90+
p.bots["fresh"] = fresh
91+
92+
p.reap(time.Now().Add(-time.Minute))
93+
94+
if !idleKilled {
95+
t.Fatal("idle bot should be reaped")
96+
}
97+
if freshKilled {
98+
t.Fatal("fresh bot should survive")
99+
}
100+
if _, ok := p.bots["idle"]; ok {
101+
t.Fatal("idle bot not removed")
102+
}
103+
if _, ok := p.bots["fresh"]; !ok {
104+
t.Fatal("fresh bot removed")
105+
}
106+
}
107+
108+
func TestRunGCReapsThenStops(t *testing.T) {
109+
p, _ := New(Options{AppID: 1, AppHash: "x", IdleTimeout: 10 * time.Millisecond})
110+
killed := false
111+
m := fakeManaged(&killed)
112+
m.lastUsed = time.Now().Add(-time.Hour)
113+
p.bots["idle"] = m
114+
115+
ctx, cancel := context.WithCancel(context.Background())
116+
done := make(chan struct{})
117+
go func() { p.RunGC(ctx); close(done) }()
118+
119+
// Wait for the idle bot to be reaped.
120+
deadline := time.After(2 * time.Second)
121+
for {
122+
p.mu.Lock()
123+
n := len(p.bots)
124+
p.mu.Unlock()
125+
if n == 0 {
126+
break
127+
}
128+
select {
129+
case <-deadline:
130+
t.Fatal("idle bot was not reaped")
131+
case <-time.After(5 * time.Millisecond):
132+
}
133+
}
134+
if !killed {
135+
t.Fatal("reaped bot should be killed")
136+
}
137+
138+
cancel()
139+
select {
140+
case <-done:
141+
case <-time.After(time.Second):
142+
t.Fatal("RunGC did not stop")
143+
}
144+
}
145+
146+
func TestAcquireDedupes(t *testing.T) {
147+
p, _ := New(Options{AppID: 1, AppHash: "x"})
148+
killed := false
149+
existing := fakeManaged(&killed)
150+
p.bots["123:abc"] = existing
151+
152+
tok, err := ParseToken("123:abc")
153+
if err != nil {
154+
t.Fatal(err)
155+
}
156+
got, err := p.acquire(tok)
157+
if err != nil {
158+
t.Fatalf("acquire: %v", err)
159+
}
160+
if got != existing {
161+
t.Fatal("acquire should return the existing managed bot, not start a new one")
162+
}
163+
}

0 commit comments

Comments
 (0)