Files
2026-07-13 13:00:08 +08:00

115 lines
2.9 KiB
Go

package bot
import (
"io"
"log/slog"
"sync"
"testing"
"time"
"reasonix/internal/control"
)
type closeProbeBotController struct {
*control.Controller
onClose func()
}
func (c *closeProbeBotController) Close() {
if c.onClose != nil {
c.onClose()
}
}
func TestBotGatewayStopClosesSessionsWithoutGatewayLock(t *testing.T) {
gw := &BotGateway{
controllers: map[string]*sessionState{},
}
closed := make(chan struct{}, 1)
gw.controllers["session"] = &sessionState{
ctrl: &closeProbeBotController{
Controller: control.New(control.Options{}),
onClose: func() {
gw.mu.Lock()
gw.mu.Unlock() //nolint:staticcheck // probe: lock must be immediately acquirable
closed <- struct{}{}
},
},
}
done := make(chan struct{})
go func() {
gw.Stop()
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("Stop blocked while closing a controller")
}
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("controller Close was not called")
}
if len(gw.controllers) != 0 {
t.Fatalf("controllers retained after Stop: %d", len(gw.controllers))
}
}
// Guards the gw.cfg.Channels / gw.cfg.ConnectionChannels / gw.cfg.ToolApprovalMode
// snapshot locking: approval-mode writers mutate those under gw.mu while
// sessionOptionsForMessage and the project/session index builders read them.
// Run with -race; a lock-free read is a concurrent map read/write crash.
func TestBotGatewayToolApprovalModeConcurrentWithConfigReaders(t *testing.T) {
t.Setenv("REASONIX_HOME", t.TempDir())
gw := &BotGateway{
cfg: GatewayConfig{
WorkspaceRoot: t.TempDir(),
Channels: map[Platform]ChannelConfig{
PlatformFeishu: {ToolApprovalMode: control.ToolApprovalAsk},
},
ConnectionChannels: map[string]ChannelConfig{
"feishu-lark": {ToolApprovalMode: control.ToolApprovalAsk},
},
},
controllers: map[string]*sessionState{},
sessionOverrides: map[string]sessionRuntimeOverride{},
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}
stop := make(chan struct{})
var wg sync.WaitGroup
defer func() {
close(stop)
wg.Wait()
}()
wg.Add(1)
go func() {
defer wg.Done()
modes := []string{control.ToolApprovalYolo, control.ToolApprovalAsk, control.ToolApprovalAuto}
for i := 0; ; i++ {
select {
case <-stop:
return
default:
}
mode := modes[i%len(modes)]
gw.UpdateConnectionToolApprovalMode("feishu-lark", mode)
gw.mu.Lock()
gw.updateToolApprovalModeDefaultLocked(InboundMessage{Platform: PlatformFeishu}, mode)
gw.updateToolApprovalModeDefaultLocked(InboundMessage{}, mode)
gw.mu.Unlock()
}
}()
connMsg := InboundMessage{Platform: PlatformFeishu, ConnectionID: "feishu-lark", ChatType: ChatDM, ChatID: "chat", UserID: "user"}
for i := 0; i < 200; i++ {
gw.sessionOptionsForMessage(connMsg)
gw.sessionOptionsForMessage(InboundMessage{Platform: PlatformFeishu})
projects := gw.buildProjectIndex()
gw.buildSessionIndex(projects)
}
}