Files
Cyrene_v2/internal/capability/skills/store_test.go
T

202 lines
5.6 KiB
Go

package skills
import (
"context"
"errors"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
func TestStore_UpsertGet_正常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
if err := s.Upsert(ctx, makeSkill(t, "daily-brief")); err != nil {
t.Fatalf("Upsert: %v", err)
}
got, err := s.Get(ctx, "daily-brief")
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.Name() != "daily-brief" {
t.Fatalf("Name = %q, want daily-brief", got.Name())
}
if got.Description() != "desc daily-brief" {
t.Fatalf("Description = %q, want desc daily-brief", got.Description())
}
if got.Prompt() != "prompt daily-brief" {
t.Fatalf("Prompt = %q, want prompt daily-brief", got.Prompt())
}
steps := got.Steps()
if len(steps) != 1 || steps[0].Name != "s1" || steps[0].Prompt != "step prompt" {
t.Fatalf("Steps = %+v, want one step s1/step prompt", steps)
}
refs := got.Refs()
if len(refs) != 1 || refs[0].Name != "r1" || refs[0].Type != "tool" || refs[0].Value != "v1" || refs[0].Optional {
t.Fatalf("Refs = %+v, want r1/tool/v1/non-optional", refs)
}
guards := got.Guardrails()
if len(guards) != 1 || guards[0].ID != "g1" || guards[0].Message != "msg" || !guards[0].Enforced {
t.Fatalf("Guardrails = %+v, want g1/msg/enforced", guards)
}
}
func TestStore_Upsert_Update_正常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
if err := s.Upsert(ctx, makeSkill(t, "review")); err != nil {
t.Fatalf("first Upsert: %v", err)
}
// Same name, changed description/prompt — must update in place, not duplicate.
updated, err := New("review", "v2 desc", "v2 prompt", nil, nil, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if err := s.Upsert(ctx, updated); err != nil {
t.Fatalf("second Upsert: %v", err)
}
got, err := s.Get(ctx, "review")
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.Description() != "v2 desc" || got.Prompt() != "v2 prompt" {
t.Fatalf("updated skill = %q/%q, want v2 desc/v2 prompt", got.Description(), got.Prompt())
}
all, err := s.List(ctx)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(all) != 1 {
t.Fatalf("List after update has %d rows, want 1", len(all))
}
}
func TestStore_Get_NotFound_异常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
_, err := s.Get(context.Background(), "missing")
if !errors.Is(err, ErrNotFound) {
t.Fatalf("Get(missing) err = %v, want ErrNotFound", err)
}
}
func TestStore_List_正常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
for _, n := range []string{"b", "a", "c"} {
if err := s.Upsert(ctx, makeSkill(t, n)); err != nil {
t.Fatalf("Upsert(%s): %v", n, err)
}
}
got, err := s.List(ctx)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 3 {
t.Fatalf("List length = %d, want 3", len(got))
}
if got[0].Name() != "a" || got[1].Name() != "b" || got[2].Name() != "c" {
t.Fatalf("List order = %q,%q,%q, want a,b,c", got[0].Name(), got[1].Name(), got[2].Name())
}
}
func TestStore_Delete_正常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
if err := s.Upsert(ctx, makeSkill(t, "x")); err != nil {
t.Fatalf("Upsert: %v", err)
}
if err := s.Delete(ctx, "x"); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, err := s.Get(ctx, "x"); !errors.Is(err, ErrNotFound) {
t.Fatalf("Get after Delete err = %v, want ErrNotFound", err)
}
}
func TestStore_Delete_NotFound_异常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
if err := s.Delete(context.Background(), "missing"); !errors.Is(err, ErrNotFound) {
t.Fatalf("Delete(missing) err = %v, want ErrNotFound", err)
}
}
func TestStore_Upsert_Invalid_异常(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
cases := []struct {
name string
sk sdk.Skill
}{
{name: "nil skill", sk: nil},
{name: "blank name", sk: mockSkill{name: " "}},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := s.Upsert(ctx, tc.sk); !errors.Is(err, ErrInvalidSkill) {
t.Fatalf("Upsert(%s) err = %v, want ErrInvalidSkill", tc.name, err)
}
})
}
}
// TestStore_SQLInjection_安全 stores a name/description that looks like SQL and
// verifies the values round-trip verbatim (parameterized statements), that the
// table survives, and that unrelated rows are unaffected.
func TestStore_SQLInjection_安全(t *testing.T) {
t.Parallel()
s := newTestStore(t)
ctx := context.Background()
name := "x'); DROP TABLE skills; --"
desc := "'; SELECT * FROM skills; --"
if err := s.Upsert(ctx, mockSkill{name: name}); err != nil {
t.Fatalf("Upsert injection name: %v", err)
}
got, err := s.Get(ctx, name)
if err != nil {
t.Fatalf("Get injection name: %v", err)
}
if got.Name() != name {
t.Fatalf("Name = %q, want the injection string stored verbatim", got.Name())
}
// A description carrying SQL must also be stored as-is.
sk, err := New("inject-desc", desc, "p", nil, nil, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if err := s.Upsert(ctx, sk); err != nil {
t.Fatalf("Upsert injection desc: %v", err)
}
got2, err := s.Get(ctx, "inject-desc")
if err != nil {
t.Fatalf("Get inject-desc: %v", err)
}
if got2.Description() != desc {
t.Fatalf("Description = %q, want verbatim injection", got2.Description())
}
// The table must still be intact and usable.
if err := s.Upsert(ctx, makeSkill(t, "after")); err != nil {
t.Fatalf("Upsert after injection: %v", err)
}
all, err := s.List(ctx)
if err != nil {
t.Fatalf("List after injection: %v", err)
}
if len(all) != 3 {
t.Fatalf("List after injection has %d rows, want 3", len(all))
}
}