package skills import ( "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本文件专测 design/09 §6.3 的清单投影:只有 name + description(+when-to-use)、 // 三态过滤、单条预算、条数上限**如实标 truncated**。 func TestBuildListing_三态(t *testing.T) { t.Parallel() both := mustSpecSkill(t, "both", "做 A", "在做 A 时", sdk.InvocationBoth) modelOnly := mustSpecSkill(t, "model-only", "做 B", "", sdk.InvocationModelOnly) userOnly := mustSpecSkill(t, "user-only", "做 C", "", sdk.InvocationUserOnly) listing := BuildListing([]sdk.Skill{both, modelOnly, userOnly}) if len(listing.Entries) != 3 { t.Fatalf("条目数 = %d, want 3(user-only 也要在清单里可见)", len(listing.Entries)) } if got := listing.Entries[0].Description; got != "做 A Use when: 在做 A 时" { t.Fatalf("both 描述 = %q, want 拼接 description + when-to-use", got) } if !listing.Entries[0].ModelInvocable || !listing.Entries[0].UserInvocable { t.Fatalf("both 应当人与模型都可调:%+v", listing.Entries[0]) } if listing.Entries[1].Description != "" || !listing.Entries[1].ModelInvocable || listing.Entries[1].UserInvocable { t.Fatalf("model-only 应当「清单不带描述、只有模型可调」:%+v", listing.Entries[1]) } if listing.Entries[2].Description == "" || listing.Entries[2].ModelInvocable || !listing.Entries[2].UserInvocable { t.Fatalf("user-only 应当「清单带描述、模型不可调」:%+v", listing.Entries[2]) } } // TestBuildListing_正文不进清单 断言渐进披露的硬规格:清单里没有正文(prompt / // 步骤文本)—— 上面那些字段是唯一进清单的东西。 func TestBuildListing_正文不进清单(t *testing.T) { t.Parallel() skill := mustSpecSkill(t, "s", "描述", "", sdk.InvocationBoth) listing := BuildListing([]sdk.Skill{skill}) if len(listing.Entries) != 1 { t.Fatalf("条目数 = %d, want 1", len(listing.Entries)) } if strings.Contains(listing.Entries[0].Description, "base prompt") { t.Fatalf("正文泄漏进清单:%q", listing.Entries[0].Description) } } func TestBuildListing_单条预算截断(t *testing.T) { t.Parallel() skill := mustSpecSkill(t, "long", strings.Repeat("描", SkillListingDescriptionBudget+500), "", sdk.InvocationBoth) entry := BuildListing([]sdk.Skill{skill}).Entries[0] if !entry.Truncated { t.Fatal("超预算的描述必须被标 Truncated") } if got := len([]rune(entry.Description)); got != SkillListingDescriptionBudget { t.Fatalf("截断后长度 = %d, want %d", got, SkillListingDescriptionBudget) } if !strings.HasSuffix(entry.Description, SkillListingTruncatedMarker) { t.Fatalf("截断没有显式标记:%q", entry.Description[len(entry.Description)-20:]) } } func TestBuildListing_条数上限如实标(t *testing.T) { t.Parallel() list := make([]sdk.Skill, 0, SkillListingMaxEntries+3) for i := 0; i < SkillListingMaxEntries+3; i++ { list = append(list, mustSpecSkill(t, "s"+string(rune('a'+i%26))+string(rune('0'+i/26)), "d", "", sdk.InvocationBoth)) } listing := BuildListing(list) if len(listing.Entries) != SkillListingMaxEntries { t.Fatalf("条目数 = %d, want %d", len(listing.Entries), SkillListingMaxEntries) } if !listing.Truncated || listing.Note == "" { t.Fatalf("超上限必须如实标 Truncated 且给出说明:%+v", listing) } } func TestBuildListing_跳过nil(t *testing.T) { t.Parallel() listing := BuildListing([]sdk.Skill{nil, mustSpecSkill(t, "s", "d", "", sdk.InvocationBoth), nil}) if len(listing.Entries) != 1 || listing.Truncated { t.Fatalf("Listing = %+v, want 1 条且未截断", listing) } } // mustSpecSkill 构造一个带 description/when-to-use/invocation 的技能。 func mustSpecSkill(t *testing.T, name, description, whenToUse string, invocation sdk.Invocation) *Skill { t.Helper() skill, err := New(name, description, "base prompt", nil, nil, nil, WithSpec(Spec{WhenToUse: whenToUse, Invocation: invocation})) if err != nil { t.Fatalf("New(%s): %v", name, err) } return skill }