* H-16: align takeover with channel AI workflow (#2) * feat(conversations): complete manual AI takeover * fix(conversations): align AI takeover flow with channel AI * fix(conversations): close takeover review gaps --------- Co-authored-by: Rogee <rogee@ipao.vip> * feat(shangwutong): sync customer names back to channel (#3) Co-authored-by: Rogee <rogee@ipao.vip> * fix(shangwutong): close contact sync review gaps (#4) Co-authored-by: Rogee <rogee@ipao.vip> * H-28: harden Shangwutong CID sync (#5) * fix(shangwutong): close contact sync review gaps * fix(shangwutong): harden CID sync boundaries --------- Co-authored-by: Rogee <rogee@ipao.vip> * fix(conversations): sync AI takeover exit in realtime (#6) Co-authored-by: Rogee <rogee@ipao.vip> * test(shangwutong): cover CID rename reliability (#7) Co-authored-by: Rogee <rogee@ipao.vip> * H-43: fix WEB Captain takeover E2E flow (#8) * test(shangwutong): cover CID rename reliability * H-43: fix WEB Captain takeover flow * H-48: preserve compatible provider model * H-49: make Captain takeover atomic * H-50: prevent duplicate widget initialization --------- Co-authored-by: Rogee <rogee@ipao.vip> * H-55: make Captain bindings atomic (#9) Co-authored-by: Rogee <rogee@ipao.vip> * H-60: harden Captain migration rollback and concurrency * chore(agent): baseline — uncommitted work from the local directory * H-335: add safe Captain skills and user deactivation * H-338: close auth and Captain review blockers * H-338: close assignment and session races * H-338: close assignment and websocket invalidation gaps * H-338: enforce assignment write invariants --------- Co-authored-by: Rogee <rogee@ipao.vip>
61 lines
1.6 KiB
Go
61 lines
1.6 KiB
Go
package ws
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
wspkg "github.com/gochat/gochat/internal/ws"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHubDisconnectUserClosesOnlyAgentConnections(t *testing.T) {
|
|
hub := NewHubSimple()
|
|
agent := NewClient(7, 1, nil, hub)
|
|
contact := NewClient(7, 1, nil, hub)
|
|
contact.IsContact = true
|
|
hub.Register(agent)
|
|
hub.Register(contact)
|
|
|
|
hub.DisconnectUser(7)
|
|
|
|
hub.mu.RLock()
|
|
defer hub.mu.RUnlock()
|
|
require.NotContains(t, hub.clients, agent.ID)
|
|
require.Contains(t, hub.clients, contact.ID)
|
|
}
|
|
|
|
func TestUserDisconnectBroadcastClosesConnectionOnAnotherInstance(t *testing.T) {
|
|
mr := miniredis.RunT(t)
|
|
rdbA := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
rdbB := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
t.Cleanup(func() {
|
|
require.NoError(t, rdbA.Close())
|
|
require.NoError(t, rdbB.Close())
|
|
})
|
|
hubA := NewHubSimple()
|
|
hubB := NewHubSimple()
|
|
relayA := wspkg.NewBroadcastRelay(rdbA, hubA)
|
|
relayB := wspkg.NewBroadcastRelay(rdbB, hubB)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
require.NoError(t, relayA.Stop())
|
|
require.NoError(t, relayB.Stop())
|
|
})
|
|
require.NoError(t, relayA.Start(ctx))
|
|
require.NoError(t, relayB.Start(ctx))
|
|
|
|
remoteAgent := NewClient(7, 1, nil, hubB)
|
|
hubB.Register(remoteAgent)
|
|
require.NoError(t, relayA.PublishUserDisconnect(ctx, 7))
|
|
require.Eventually(t, func() bool {
|
|
hubB.mu.RLock()
|
|
defer hubB.mu.RUnlock()
|
|
_, connected := hubB.clients[remoteAgent.ID]
|
|
return !connected
|
|
}, time.Second, 10*time.Millisecond)
|
|
}
|