- 加载器/校验:目录式 configs/skills/<id>/skill.yaml(兼容旧单文件 <id>.yaml,先目录后文件); 新字段 version/when-to-use/invocation(三态)/triggers/paths/allowed-tools/disallowed-tools/ context(ine|fork)/agent/model/effort/metadata;**依赖环 ⇒ 加载期拒**(运行期绝不挂死); depends 指向不存在步骤 ⇒ 拒;同一工具同时在 allowed 与 disallowed ⇒ 拒;错误可分类(哨兵 + %w) - Runner.Run 真执行(替换 ErrRunPending):步骤拓扑(无依赖可并行、上限 2)+ guardrail 强制点 (enforced 违反 ⇒ 中止并如实报「哪条、哪一步」;否则 WARN 继续)+ 失败不静默跳过 + ctx 取消传播 - 🔴 能力收缩只收不放:该轮可用工具 = allowed-tools ∩ 安全闸 ∩ 注册表,再减 disallowed-tools - 另有渐进披露相关的 listing/surface/activation(清单投影与激活判定) ⚠️ 未接线:本片是库与执行器,尚未把技能清单接进模型面(见 report 的分阶段)。
99 lines
4.0 KiB
Go
99 lines
4.0 KiB
Go
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
|
||
}
|