243 lines
6.7 KiB
Go
243 lines
6.7 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// texts builds a context of text-only messages with the given body texts.
|
|
func texts(role model.Role, bodies ...string) []model.Message {
|
|
out := make([]model.Message, 0, len(bodies))
|
|
for _, b := range bodies {
|
|
out = append(out, model.TextMessage(role, b))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sameMessage compares a message by role and concatenated text; the test
|
|
// messages are text-only, so this is sufficient for equality.
|
|
func sameMessage(a, b model.Message) bool {
|
|
return a.Role == b.Role && messageText(a) == messageText(b)
|
|
}
|
|
|
|
func TestNewContextAuditor_Snapshot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msgs := texts(model.RoleUser, "alpha", "bravo", "charlie")
|
|
aud := NewContextAuditor(msgs)
|
|
|
|
// Mutating the caller's slice after construction must not affect the audit.
|
|
msgs[0].Role = model.RoleSystem
|
|
got := aud.Full()
|
|
if got[0].Role != model.RoleUser {
|
|
t.Fatalf("snapshot leaked caller mutation: role = %q, want %q", got[0].Role, model.RoleUser)
|
|
}
|
|
}
|
|
|
|
func TestFull_ReturnsDefensiveCopy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo"))
|
|
|
|
got := aud.Full()
|
|
got[0].Role = model.RoleSystem
|
|
got[1].Role = model.RoleSystem
|
|
|
|
fresh := aud.Full()
|
|
if fresh[0].Role != model.RoleUser || fresh[1].Role != model.RoleUser {
|
|
t.Fatalf("Full leaked mutation of its returned copy: %+v", fresh)
|
|
}
|
|
}
|
|
|
|
func TestTail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
full := texts(model.RoleUser, "alpha", "bravo", "charlie", "delta")
|
|
aud := NewContextAuditor(full)
|
|
|
|
tests := []struct {
|
|
name string
|
|
n int
|
|
want []string
|
|
}{
|
|
{name: "zero returns empty", n: 0, want: nil},
|
|
{name: "negative returns empty", n: -1, want: nil},
|
|
{name: "single", n: 1, want: []string{"delta"}},
|
|
{name: "pair", n: 2, want: []string{"charlie", "delta"}},
|
|
{name: "exact length", n: 4, want: []string{"alpha", "bravo", "charlie", "delta"}},
|
|
{name: "beyond length clamps to full", n: 9, want: []string{"alpha", "bravo", "charlie", "delta"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := aud.Tail(tt.n)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("Tail(%d) length = %d, want %d", tt.n, len(got), len(tt.want))
|
|
}
|
|
for i := range got {
|
|
if got[i].Role != model.RoleUser || messageText(got[i]) != tt.want[i] {
|
|
t.Fatalf("Tail(%d)[%d] = %q/%s, want text %q", tt.n, i, messageText(got[i]), got[i].Role, tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKeyword_LocateWithSurroundings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo", "charlie"))
|
|
|
|
loc, err := aud.Keyword(context.Background(), "rav")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc == nil {
|
|
t.Fatal("Keyword returned nil for a present match")
|
|
}
|
|
if loc.Line != 2 {
|
|
t.Fatalf("Line = %d, want 2", loc.Line)
|
|
}
|
|
if len(loc.Surrounding) != 3 {
|
|
t.Fatalf("Surrounding length = %d, want 3", len(loc.Surrounding))
|
|
}
|
|
if !sameMessage(loc.Surrounding[1], model.TextMessage(model.RoleUser, "bravo")) {
|
|
t.Fatalf("matched message missing from window: %+v", loc.Surrounding)
|
|
}
|
|
if !sameMessage(loc.Surrounding[0], model.TextMessage(model.RoleUser, "alpha")) {
|
|
t.Fatalf("above-message missing from window: %+v", loc.Surrounding)
|
|
}
|
|
if !sameMessage(loc.Surrounding[2], model.TextMessage(model.RoleUser, "charlie")) {
|
|
t.Fatalf("below-message missing from window: %+v", loc.Surrounding)
|
|
}
|
|
}
|
|
|
|
func TestKeyword_Edges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("first message has no above-neighbor", func(t *testing.T) {
|
|
t.Parallel()
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo"))
|
|
loc, err := aud.Keyword(context.Background(), "alp")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc.Line != 1 {
|
|
t.Fatalf("Line = %d, want 1", loc.Line)
|
|
}
|
|
if len(loc.Surrounding) != 2 {
|
|
t.Fatalf("Surrounding length = %d, want 2", len(loc.Surrounding))
|
|
}
|
|
})
|
|
|
|
t.Run("last message has no below-neighbor", func(t *testing.T) {
|
|
t.Parallel()
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo"))
|
|
loc, err := aud.Keyword(context.Background(), "bra")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc.Line != 2 {
|
|
t.Fatalf("Line = %d, want 2", loc.Line)
|
|
}
|
|
if len(loc.Surrounding) != 2 {
|
|
t.Fatalf("Surrounding length = %d, want 2", len(loc.Surrounding))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestKeyword_CaseInsensitive(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo"))
|
|
loc, err := aud.Keyword(context.Background(), "BRAVO")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc == nil || loc.Line != 2 {
|
|
t.Fatalf("case-insensitive match failed: line = %d", loc.Line)
|
|
}
|
|
}
|
|
|
|
func TestKeyword_NoMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha", "bravo"))
|
|
loc, err := aud.Keyword(context.Background(), "zzz")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc != nil {
|
|
t.Fatalf("Keyword = %+v, want nil for a missing match", loc)
|
|
}
|
|
}
|
|
|
|
func TestKeyword_EmptyAndBlankQuery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha"))
|
|
for _, q := range []string{"", " "} {
|
|
_, err := aud.Keyword(context.Background(), q)
|
|
if !errors.Is(err, ErrQueryEmpty) {
|
|
t.Fatalf("Keyword(%q) error = %v, want ErrQueryEmpty", q, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKeyword_CanceledContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "alpha"))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := aud.Keyword(ctx, "alpha")
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Keyword error = %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestKeyword_FirstMatchOnly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
aud := NewContextAuditor(texts(model.RoleUser, "foo", "bar", "foo"))
|
|
loc, err := aud.Keyword(context.Background(), "foo")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc.Line != 1 {
|
|
t.Fatalf("Line = %d, want 1 (earliest match)", loc.Line)
|
|
}
|
|
}
|
|
|
|
func TestKeyword_IgnoresImagePayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msgs := []model.Message{
|
|
model.TextMessage(model.RoleUser, "alpha"),
|
|
model.ImageMessage(model.RoleUser, "", "http://example.com/bravo.png"),
|
|
}
|
|
aud := NewContextAuditor(msgs)
|
|
|
|
loc, err := aud.Keyword(context.Background(), "bravo")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc != nil {
|
|
t.Fatalf("Keyword matched an image URL (ignored): %+v", loc)
|
|
}
|
|
|
|
loc, err = aud.Keyword(context.Background(), "alpha")
|
|
if err != nil {
|
|
t.Fatalf("Keyword unexpected error: %v", err)
|
|
}
|
|
if loc == nil || loc.Line != 1 {
|
|
t.Fatalf("text-part match failed: line = %d", loc.Line)
|
|
}
|
|
}
|