115 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|