mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-04 10:13:16 +00:00
155 lines
6.5 KiB
Go
155 lines
6.5 KiB
Go
package replycontext
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCacheBuild_MultiLevelReplyChain(t *testing.T) {
|
|
cache := NewCache(Options{MaxDepth: 8, MaxTotalChars: 6000, MaxCharsPerMessage: 1000})
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scope, Message{IDs: []string{"a"}, Sender: "Alice", Body: "original"})
|
|
cache.Store(scope, Message{IDs: []string{"b"}, ParentIDs: []string{"a"}, Sender: "Bob", Body: "reply 1"})
|
|
cache.Store(scope, Message{IDs: []string{"c"}, ParentIDs: []string{"b"}, Sender: "Carol", Body: "reply 2"})
|
|
cache.Store(scope, Message{IDs: []string{"d"}, ParentIDs: []string{"c"}, Sender: "Dan", Body: "reply 3"})
|
|
cache.Store(scope, Message{IDs: []string{"e"}, ParentIDs: []string{"d"}, Sender: "Eve", Body: "reply 4"})
|
|
cache.Store(scope, Message{IDs: []string{"f"}, ParentIDs: []string{"e"}, Sender: "Frank", Body: "reply 5"})
|
|
|
|
got := cache.Build(scope, Quote{IDs: []string{"f"}, Sender: "Frank", Body: "direct fallback"})
|
|
for _, want := range []string{"original", "reply 1", "reply 2", "reply 3", "reply 4", "reply 5"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("missing %q in %q", want, got)
|
|
}
|
|
}
|
|
assertOrder(t, got, "original", "reply 1", "reply 2", "reply 3", "reply 4", "reply 5")
|
|
if strings.Contains(got, "direct fallback") {
|
|
t.Fatalf("used fallback despite cache hit: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCacheBuild_MultiLevelReplyChainWithUncachedRootQuote(t *testing.T) {
|
|
cache := NewCache(Options{MaxDepth: 8, MaxTotalChars: 6000, MaxCharsPerMessage: 1000})
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scope, Message{
|
|
IDs: []string{"reply-1"},
|
|
ParentIDs: []string{"root"},
|
|
ParentQuote: Quote{
|
|
IDs: []string{"root"},
|
|
Sender: "Agent",
|
|
Body: "original outbound message",
|
|
},
|
|
Sender: "Alice",
|
|
Body: "reply 1",
|
|
})
|
|
cache.Store(scope, Message{IDs: []string{"reply-2"}, ParentIDs: []string{"reply-1"}, Sender: "Bob", Body: "reply 2"})
|
|
cache.Store(scope, Message{IDs: []string{"reply-3"}, ParentIDs: []string{"reply-2"}, Sender: "Carol", Body: "reply 3"})
|
|
cache.Store(scope, Message{IDs: []string{"reply-4"}, ParentIDs: []string{"reply-3"}, Sender: "Dan", Body: "reply 4"})
|
|
cache.Store(scope, Message{IDs: []string{"reply-5"}, ParentIDs: []string{"reply-4"}, Sender: "Eve", Body: "reply 5"})
|
|
cache.Store(scope, Message{IDs: []string{"reply-6"}, ParentIDs: []string{"reply-5"}, Sender: "Frank", Body: "reply 6"})
|
|
|
|
got := cache.Build(scope, Quote{IDs: []string{"reply-6"}, Sender: "Frank", Body: "direct fallback"})
|
|
for _, want := range []string{"original outbound message", "reply 1", "reply 2", "reply 3", "reply 4", "reply 5", "reply 6"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("missing %q in %q", want, got)
|
|
}
|
|
}
|
|
assertOrder(t, got, "original outbound message", "reply 1", "reply 2", "reply 3", "reply 4", "reply 5", "reply 6")
|
|
if strings.Contains(got, "direct fallback") {
|
|
t.Fatalf("used fallback despite cache hit: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCacheBuild_FallbackOnCacheMiss(t *testing.T) {
|
|
cache := NewCache(Options{})
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
got := cache.Build(scope, Quote{IDs: []string{"missing"}, Sender: "Alice", Body: "quoted text"})
|
|
want := "[Replying to Alice]\nquoted text\n[/Replying]"
|
|
if got != want {
|
|
t.Fatalf("Build() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCacheBuild_IsolatedByScope(t *testing.T) {
|
|
cache := NewCache(Options{})
|
|
scopeA := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
scopeB := Scope{TenantID: "tenant-b", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scopeA, Message{IDs: []string{"same-id"}, Sender: "Alice", Body: "tenant a secret"})
|
|
got := cache.Build(scopeB, Quote{IDs: []string{"same-id"}, Sender: "Bob", Body: "tenant b fallback"})
|
|
if strings.Contains(got, "tenant a secret") {
|
|
t.Fatalf("cross-scope context leaked: %q", got)
|
|
}
|
|
if !strings.Contains(got, "tenant b fallback") {
|
|
t.Fatalf("missing fallback in %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCacheBuild_LimitsDepthAndStopsCycles(t *testing.T) {
|
|
cache := NewCache(Options{MaxDepth: 2, MaxTotalChars: 6000, MaxCharsPerMessage: 1000})
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scope, Message{IDs: []string{"a"}, ParentIDs: []string{"c"}, Sender: "Alice", Body: "a"})
|
|
cache.Store(scope, Message{IDs: []string{"b"}, ParentIDs: []string{"a"}, Sender: "Bob", Body: "b"})
|
|
cache.Store(scope, Message{IDs: []string{"c"}, ParentIDs: []string{"b"}, Sender: "Carol", Body: "c"})
|
|
|
|
got := cache.Build(scope, Quote{IDs: []string{"c"}, Sender: "Carol", Body: "fallback"})
|
|
if blocks := strings.Count(got, "[Replying to"); blocks != 2 {
|
|
t.Fatalf("rendered %d blocks, want depth limit 2: %q", blocks, got)
|
|
}
|
|
}
|
|
|
|
func TestCacheBuild_EnforcesCharacterCaps(t *testing.T) {
|
|
cache := NewCache(Options{MaxDepth: 8, MaxTotalChars: 120, MaxCharsPerMessage: 32})
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scope, Message{IDs: []string{"a"}, Sender: "Alice", Body: strings.Repeat("a", 200)})
|
|
got := cache.Build(scope, Quote{IDs: []string{"a"}})
|
|
if len(got) > 120 {
|
|
t.Fatalf("rendered %d bytes, want <= 120: %q", len(got), got)
|
|
}
|
|
if !strings.Contains(got, "...") {
|
|
t.Fatalf("missing truncation marker: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCacheEvictsExpiredMessagesAndClear(t *testing.T) {
|
|
cache := NewCache(Options{TTL: time.Minute})
|
|
now := time.Date(2026, 7, 2, 20, 0, 0, 0, time.UTC)
|
|
cache.now = func() time.Time { return now }
|
|
scope := Scope{TenantID: "tenant-a", ChannelID: "zalo-main", ThreadID: "thread-1"}
|
|
|
|
cache.Store(scope, Message{IDs: []string{"a"}, Sender: "Alice", Body: "old"})
|
|
cache.now = func() time.Time { return now.Add(2 * time.Minute) }
|
|
|
|
got := cache.Build(scope, Quote{IDs: []string{"a"}, Sender: "Alice", Body: "fallback"})
|
|
if strings.Contains(got, "old") {
|
|
t.Fatalf("expired context rendered: %q", got)
|
|
}
|
|
|
|
cache.Store(scope, Message{IDs: []string{"b"}, Sender: "Bob", Body: "fresh"})
|
|
cache.Clear()
|
|
got = cache.Build(scope, Quote{IDs: []string{"b"}, Sender: "Bob", Body: "fallback"})
|
|
if strings.Contains(got, "fresh") {
|
|
t.Fatalf("cleared context rendered: %q", got)
|
|
}
|
|
}
|
|
|
|
func assertOrder(t *testing.T, haystack string, values ...string) {
|
|
t.Helper()
|
|
last := -1
|
|
for _, value := range values {
|
|
idx := strings.Index(haystack, value)
|
|
if idx < 0 {
|
|
t.Fatalf("missing %q in %q", value, haystack)
|
|
}
|
|
if idx <= last {
|
|
t.Fatalf("%q appeared out of order in %q", value, haystack)
|
|
}
|
|
last = idx
|
|
}
|
|
}
|