feat(capability/skills): 可复用工作流技能(Skill 模型 + Store + YAML Loader + Runner 展开 + 注册)
This commit is contained in:
1 parent
d947ec41a9
commit
915aa8a100
10 files changed
+1235
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user