package audit import ( "context" "errors" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 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 按角色与拼接后的文本比较两条消息;测试消息都是纯文本,因此这足以判定 // 相等。 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) // 构造之后再改调用方的切片,不得影响审计结果。 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) } }