Files
Agent-Native/runtime/skills/frontmatter_test.go
weichen e1fac5864d
ci / vet-and-test (pull_request) Successful in 1m1s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Validate skills against the open Agent Skills spec (N2 WP4)
Why: the completion doc's N2 requires skills validated per the open
spec; today frontmatter was a description-only line hack with silent
skips, no name field, and no error surface.
Evidence: GOWORK=off go vet + go test -count=1 ./... green; parser
units cover all field forms and reject malformed shapes with located
errors; the validation matrix covers kebab-case/name-equals-dir/
required description/limits; Store.Diagnostics surfaces per-skill
reasons and Load returns the located validation error for same-named
invalid skills; existing fixtures updated to spec form.
Impact: stricter by design — skills without name/description or with
unknown frontmatter keys now produce located errors instead of silently
loading or silently skipping; ValidateSkillDocument is exported so the
product plugin/seed paths share the same validator.
Refs: Agent-System-Documents#42 (N2 plan WP4)
2026-10-07 13:51:10 +00:00

111 lines
4.0 KiB
Go

package skills
import (
"strings"
"testing"
)
// ---- N2 WP4:开放规范校验矩阵 ----
func TestParseFrontmatterAllFields(t *testing.T) {
text := "---\n" +
"name: docx-author\n" +
"description: 生成 Word\n" +
"license: Apache-2.0\n" +
"compatibility: 需要 python-docx\n" +
"metadata:\n" +
" author: 交付组\n" +
" version: \"1.0\"\n" +
"allowed-tools: read_file write_file\n" +
"---\n\n正文"
fm, body, err := parseFrontmatter("x/SKILL.md", text)
if err != nil {
t.Fatal(err)
}
if fm.Name != "docx-author" || fm.Description != "生成 Word" || fm.License != "Apache-2.0" ||
fm.Compatibility != "需要 python-docx" || body == "" || !strings.Contains(body, "正文") {
t.Fatalf("%+v body=%q", fm, body)
}
if fm.Metadata["author"] != "交付组" || fm.Metadata["version"] != "\"1.0\"" {
t.Fatalf("metadata: %+v", fm.Metadata)
}
if len(fm.AllowedTools) != 2 || fm.AllowedTools[0] != "read_file" || fm.AllowedTools[1] != "write_file" {
t.Fatalf("allowed-tools: %+v", fm.AllowedTools)
}
}
func TestValidateFrontmatterMatrix(t *testing.T) {
mk := func(mutate func(*Frontmatter)) *Frontmatter {
fm := &Frontmatter{Name: "good-name", Description: "d", Metadata: map[string]string{}}
mutate(fm)
return fm
}
cases := []struct {
name string
fm *Frontmatter
dir string
wantKey string
}{
{"缺 name", mk(func(f *Frontmatter) { f.Name = "" }), "good-name", "name"},
{"name 大写", mk(func(f *Frontmatter) { f.Name = "Bad-Name" }), "good-name", "name"},
{"name 下划线", mk(func(f *Frontmatter) { f.Name = "bad_name" }), "good-name", "name"},
{"name 连续连字符", mk(func(f *Frontmatter) { f.Name = "bad--name" }), "good-name", "name"},
{"name 尾连字符", mk(func(f *Frontmatter) { f.Name = "bad-" }), "good-name", "name"},
{"name≠目录", mk(func(f *Frontmatter) {}), "other-dir", "name"},
{"缺 description", mk(func(f *Frontmatter) { f.Description = " " }), "good-name", "description"},
{"description 超长", mk(func(f *Frontmatter) { f.Description = strings.Repeat("长", 1025) }), "good-name", "description"},
{"compatibility 超长", mk(func(f *Frontmatter) { f.Compatibility = strings.Repeat("长", 501) }), "good-name", "compatibility"},
}
for _, tc := range cases {
err := validateFrontmatter("p/SKILL.md", tc.dir, tc.fm)
ve, ok := err.(ValidationError)
if err == nil || !ok || ve.Field != tc.wantKey {
t.Fatalf("%s: 期望 %s 字段错误,得到 %v", tc.name, tc.wantKey, err)
}
if !strings.Contains(ve.Error(), "p/SKILL.md") {
t.Fatalf("%s: 错误必须带路径:%v", tc.name, ve)
}
}
// 合法形态通过。
if err := validateFrontmatter("p/SKILL.md", "good-name", mk(func(f *Frontmatter) {})); err != nil {
t.Fatalf("合法形态被误拒:%v", err)
}
}
func TestParseFrontmatterRejectsBadShapes(t *testing.T) {
cases := map[string]string{
"未知键": "---\nname: a\ndescription: d\nbogus: x\n---\n",
"非 kv 行": "---\nname: a\n这是垃圾\n---\n",
"metadata 行内值": "---\nname: a\ndescription: d\nmetadata: {a: b}\n---\n",
"allowed-tools 空": "---\nname: a\ndescription: d\nallowed-tools:\n---\n",
"无结束围栏": "---\nname: a\n",
}
for name, text := range cases {
if _, _, err := parseFrontmatter("p/SKILL.md", text); err == nil {
t.Fatalf("%s:应拒绝", name)
} else if ve, ok := err.(ValidationError); !ok || ve.Msg == "" {
t.Fatalf("%s:错误必须带定位:%v", name, err)
}
}
}
func TestStoreDiagnosticsSurface(t *testing.T) {
root := t.TempDir()
writeSkill(t, root, "good", "x")
writeRawSkill(t, root, "bad-skill", "---\nname: bad-skill\n---\n缺 description")
store := NewStore(root)
list := store.List()
if len(list) != 1 {
t.Fatalf("good only: %d", len(list))
}
diags := store.Diagnostics()
if len(diags) != 1 || diags[0].Field != "description" {
t.Fatalf("diagnostics: %+v", diags)
}
// Load 同名返回诊断原文(含路径与字段)。
_, err := store.Load("bad-skill")
if err == nil || !strings.Contains(err.Error(), "description") {
t.Fatalf("load error: %v", err)
}
}