Files
Rogeeandrogee 6c78820a1f H-338: close H-335 release blockers (#59)
* 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>
2026-08-20 10:21:19 +08:00

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)
}