- 加载器/校验:目录式 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 的分阶段)。
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
package skills
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// 本文件专测 design/09 §6.2 的 triggers / paths 判定语义(自动激活)。
|
|
|
|
func TestMatchGlob_表驱动(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
pattern string
|
|
file string
|
|
want bool
|
|
}{
|
|
{"src/**/*.ts", "src/a.ts", true},
|
|
{"src/**/*.ts", "src/a/b.ts", true},
|
|
{"src/**/*.ts", "test/a.ts", false},
|
|
{"**/*.go", "/repo/internal/x.go", true},
|
|
{"*.go", "internal/x.go", true}, // 模式不含 /:额外按文件名匹配
|
|
{"*.go", "internal/x.ts", false}, // 扩展名不符
|
|
{"internal/*.go", "internal/x.go", true},
|
|
{"internal/*.go", "internal/sub/x.go", false}, // * 不跨目录分隔符
|
|
{"**", "anything/at/all", true},
|
|
{"internal\\x.go", "internal/x.go", true}, // Windows 反斜杠归一
|
|
{"", "x.go", false},
|
|
{"x.go", "", false},
|
|
{"[bad", "x.go", false}, // 语法非法的模式一律 false
|
|
}
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.pattern+"|"+tc.file, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := MatchGlob(tc.pattern, tc.file); got != tc.want {
|
|
t.Fatalf("MatchGlob(%q, %q) = %v, want %v", tc.pattern, tc.file, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchPaths_语义(t *testing.T) {
|
|
t.Parallel()
|
|
if MatchPaths(nil, []string{"a.go"}) {
|
|
t.Fatal("未声明 paths 时不该命中")
|
|
}
|
|
if MatchPaths([]string{"*.go"}, nil) {
|
|
t.Fatal("没有工作文件时不该命中")
|
|
}
|
|
if !MatchPaths([]string{"*.md", "*.go"}, []string{"notes.txt", "main.go"}) {
|
|
t.Fatal("任一模式命中任一文件即应命中")
|
|
}
|
|
}
|
|
|
|
func TestMatchTriggers_大小写不敏感(t *testing.T) {
|
|
t.Parallel()
|
|
if MatchTriggers(nil, "帮我写简报") {
|
|
t.Fatal("未声明 triggers 时不该命中")
|
|
}
|
|
if !MatchTriggers([]string{"简报", " 汇总 "}, "帮我写一份简报") {
|
|
t.Fatal("触发词应当命中(含去空白)")
|
|
}
|
|
if !MatchTriggers([]string{"BRIEF"}, "make a brief") {
|
|
t.Fatal("触发词应当大小写不敏感")
|
|
}
|
|
if MatchTriggers([]string{"简报"}, "") {
|
|
t.Fatal("空文本不该命中")
|
|
}
|
|
}
|
|
|
|
func TestAutoActivates_语义(t *testing.T) {
|
|
t.Parallel()
|
|
if AutoActivates(nil, "x", nil) {
|
|
t.Fatal("nil 技能不该自动激活")
|
|
}
|
|
plain := mustSpecSkill(t, "plain", "d", "", sdk.InvocationBoth)
|
|
if AutoActivates(plain, "简报", []string{"x.go"}) {
|
|
t.Fatal("未声明 triggers/paths 时不该自动激活(只能显式调用)")
|
|
}
|
|
triggered := mustTriggerSkill(t, "t", []string{"简报"}, nil)
|
|
if !AutoActivates(triggered, "给我一份简报", nil) {
|
|
t.Fatal("触发词命中即应自动激活")
|
|
}
|
|
if AutoActivates(triggered, "无关的话", nil) {
|
|
t.Fatal("触发词没命中时不该自动激活")
|
|
}
|
|
pathed := mustTriggerSkill(t, "p", nil, []string{"src/**/*.go"})
|
|
if !AutoActivates(pathed, "", []string{"src/a/b.go"}) {
|
|
t.Fatal("paths 命中即应自动激活")
|
|
}
|
|
if AutoActivates(pathed, "", []string{"docs/a.md"}) {
|
|
t.Fatal("paths 没命中时不该自动激活")
|
|
}
|
|
}
|
|
|
|
func mustTriggerSkill(t *testing.T, name string, triggers, paths []string) *Skill {
|
|
t.Helper()
|
|
skill, err := New(name, "d", "p", nil, nil, nil, WithSpec(Spec{Triggers: triggers, Paths: paths}))
|
|
if err != nil {
|
|
t.Fatalf("New(%s): %v", name, err)
|
|
}
|
|
return skill
|
|
}
|