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) } }