消除跨 ContextManager 实例共享;超过容量时淘汰 2h 陈旧回合。 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
2.9 KiB
Go
111 lines
2.9 KiB
Go
package ctxmgr
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAcquireChatTurn_DuplicateSameBody(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
|
ctx := context.Background()
|
|
sessionID := "sess-dup"
|
|
turnID := "turn-abc"
|
|
bodyHash := HashTurnBody(sessionID, "hello world")
|
|
|
|
acq1, err := mgr.AcquireChatTurn(ctx, sessionID, turnID, bodyHash, "req-1", 1, 1)
|
|
if err != nil || !acq1.WonLease {
|
|
t.Fatalf("first acquire should win lease: %v %+v", err, acq1)
|
|
}
|
|
|
|
acq2, err := mgr.AcquireChatTurn(ctx, sessionID, turnID, bodyHash, "req-2", 1, 1)
|
|
if err != nil {
|
|
t.Fatalf("second acquire err: %v", err)
|
|
}
|
|
if acq2.WonLease {
|
|
t.Fatal("second acquire must not win lease")
|
|
}
|
|
if acq2.Record.AttemptCount < 2 {
|
|
t.Fatalf("attempt_count should increment, got %d", acq2.Record.AttemptCount)
|
|
}
|
|
}
|
|
|
|
func TestAcquireChatTurn_ConcurrentDuplicate(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
|
ctx := context.Background()
|
|
sessionID := "sess-conc"
|
|
turnID := "turn-conc"
|
|
bodyHash := HashTurnBody(sessionID, "concurrent msg")
|
|
|
|
var wg sync.WaitGroup
|
|
wins := 0
|
|
var mu sync.Mutex
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
acq, err := mgr.AcquireChatTurn(ctx, sessionID, turnID, bodyHash, "req", 1, 1)
|
|
if err != nil {
|
|
t.Errorf("acquire %d: %v", n, err)
|
|
return
|
|
}
|
|
if acq.WonLease {
|
|
mu.Lock()
|
|
wins++
|
|
mu.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if wins != 1 {
|
|
t.Fatalf("expected exactly 1 winner, got %d", wins)
|
|
}
|
|
}
|
|
|
|
func TestAcquireChatTurn_DifferentBodyRejected(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
|
ctx := context.Background()
|
|
sessionID := "sess-reuse"
|
|
turnID := "turn-x"
|
|
|
|
_, err := mgr.AcquireChatTurn(ctx, sessionID, turnID, HashTurnBody(sessionID, "msg-a"), "req-1", 1, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = mgr.AcquireChatTurn(ctx, sessionID, turnID, HashTurnBody(sessionID, "msg-b"), "req-2", 1, 1)
|
|
if err != ErrIdempotencyKeyReused {
|
|
t.Fatalf("expected ErrIdempotencyKeyReused, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAcquireChatTurn_StaleEntriesPruned(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
|
ctx := context.Background()
|
|
mgr.mu.Lock()
|
|
now := time.Now()
|
|
for i := 0; i < maxMemTurns+50; i++ {
|
|
key := turnMemKey("sess-prune", "old-"+strconv.Itoa(i))
|
|
mgr.turns[key] = &ChatTurnRecord{
|
|
ID: int64(i + 1),
|
|
SessionID: "sess-prune",
|
|
ClientTurnID: "old-" + strconv.Itoa(i),
|
|
Status: TurnStatusCompleted,
|
|
UpdatedAt: now.Add(-3 * time.Hour),
|
|
}
|
|
}
|
|
mgr.mu.Unlock()
|
|
bodyHash := HashTurnBody("sess-prune", "fresh")
|
|
_, err := mgr.AcquireChatTurn(ctx, "sess-prune", "fresh-turn", bodyHash, "req-fresh", 1, 1)
|
|
if err != nil {
|
|
t.Fatalf("acquire: %v", err)
|
|
}
|
|
mgr.mu.RLock()
|
|
n := len(mgr.turns)
|
|
mgr.mu.RUnlock()
|
|
if n > maxMemTurns {
|
|
t.Fatalf("expected turns pruned under capacity, got %d", n)
|
|
}
|
|
}
|