fix(skills): 技能文件包 - 修复元数据截断超限并补充初始化回归
This commit is contained in:
@@ -827,14 +827,32 @@ func parseSkillPackageMetadata(data []byte) skillPackageMetadata {
|
||||
}
|
||||
paragraph = append(paragraph, trimmed)
|
||||
}
|
||||
metadata.Description = kernel.TruncateRunes(strings.Join(paragraph, " "), 500)
|
||||
metadata.Description = strings.Join(paragraph, " ")
|
||||
}
|
||||
metadata.Name = kernel.TruncateRunes(strings.TrimSpace(metadata.Name), 80)
|
||||
metadata.Description = kernel.TruncateRunes(strings.TrimSpace(metadata.Description), 500)
|
||||
metadata.Version = kernel.TruncateRunes(strings.TrimSpace(metadata.Version), 64)
|
||||
metadata.Name = truncateSkillMetadata(metadata.Name, 80)
|
||||
metadata.Description = truncateSkillMetadata(metadata.Description, 500)
|
||||
metadata.Version = truncateSkillMetadata(metadata.Version, 64)
|
||||
return metadata
|
||||
}
|
||||
|
||||
// truncateSkillMetadata keeps the result within the validation limit. The
|
||||
// general-purpose TruncateRunes helper appends "...", which is useful for
|
||||
// display snippets but would make a value exactly at the limit invalid.
|
||||
func truncateSkillMetadata(value string, limit int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if limit <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= limit {
|
||||
return value
|
||||
}
|
||||
if limit <= 3 {
|
||||
return string(runes[:limit])
|
||||
}
|
||||
return string(runes[:limit-3]) + "..."
|
||||
}
|
||||
|
||||
func yamlScalar(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) >= 2 && value[0] == '"' {
|
||||
|
||||
@@ -3,6 +3,7 @@ package skills
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
@@ -26,6 +27,52 @@ func TestArchiveFromMarkdownInfersMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFromMarkdownTruncatesInferredDescriptionWithinLimit(t *testing.T) {
|
||||
longDescription := bytes.Repeat([]byte("描述内容。"), 140)
|
||||
data := append([]byte("# 风格库四级匹配序\n\n"), longDescription...)
|
||||
|
||||
archive, err := archiveFromMarkdown(data, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len([]rune(archive.Metadata.Description)); got > 500 {
|
||||
t.Fatalf("inferred description length = %d, want <= 500", got)
|
||||
}
|
||||
if archive.Metadata.Description == "" {
|
||||
t.Fatal("inferred description is empty")
|
||||
}
|
||||
if !bytes.Equal(archive.Files["SKILL.md"], data) {
|
||||
t.Fatal("metadata truncation changed the original instruction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillMetadataLengthBoundaries(t *testing.T) {
|
||||
for _, length := range []int{499, 500, 501, 553} {
|
||||
value := strings.Repeat("文", length)
|
||||
markdown := []byte("# 技能\n\n" + value)
|
||||
archive, err := archiveFromMarkdown(markdown, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("description length %d: %v", length, err)
|
||||
}
|
||||
want := value
|
||||
if length > 500 {
|
||||
want = strings.Repeat("文", 497) + "..."
|
||||
}
|
||||
if archive.Metadata.Description != want {
|
||||
t.Fatalf("description length %d: unexpected truncation", length)
|
||||
}
|
||||
}
|
||||
metadata := parseSkillPackageMetadata([]byte("---\nname: " + strings.Repeat("名", 81) + "\ndescription: " + strings.Repeat("文", 501) + "\nmetadata:\n version: " + strings.Repeat("版", 65) + "\n---\n"))
|
||||
if len([]rune(metadata.Name)) != 80 || len([]rune(metadata.Description)) != 500 || len([]rune(metadata.Version)) != 64 {
|
||||
t.Fatalf("metadata exceeds bounds: %#v", metadata)
|
||||
}
|
||||
// Supplied metadata still goes through strict validation; no widening of
|
||||
// the archive contract is needed to fix inferred display metadata.
|
||||
if _, err := finalizeSkillArchive(map[string][]byte{"SKILL.md": []byte("# 技能")}, skillPackageMetadata{Name: "技能", Description: strings.Repeat("文", 501)}); err == nil {
|
||||
t.Fatal("expected overlong archive metadata to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFromZipNormalizesWrapperAndNestedFiles(t *testing.T) {
|
||||
data := skillZip(t, map[string]string{
|
||||
"director-main/SKILL.md": "---\nname: AI 导演\ndescription: 导演工作流\nmetadata:\n version: 2.1\n---\n",
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
if err := db.AutoMigrate(&model.Skill{}, &model.UserSkillState{}); err != nil {
|
||||
if err := db.AutoMigrate(&model.Skill{}, &model.UserSkillState{}, &model.SkillVersion{}, &model.SkillFile{}, &model.User{}, &model.UserIdentity{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := New(repository.New(db), t.TempDir(), nil)
|
||||
@@ -55,6 +55,9 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) {
|
||||
if err := svc.EnsureBuiltinSkills(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.EnsureSkillPackages(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := model.UserSkillState{ID: "test-state", UserID: "test-user", SkillID: "16000000000001", Added: true, Liked: true}
|
||||
if err := db.Create(&state).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -62,6 +65,9 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) {
|
||||
if err := svc.EnsureBuiltinSkills(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.EnsureSkillPackages(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&model.Skill{}).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -83,4 +89,32 @@ func TestBuiltinSeedSyncPreservesUserState(t *testing.T) {
|
||||
if skill.OwnerID != "community-itswyatt-k" || skill.CreatedAt.Year() != 2026 {
|
||||
t.Fatalf("invalid persisted seed: %#v", skill)
|
||||
}
|
||||
var persisted []model.Skill
|
||||
if err := db.Find(&persisted).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, item := range persisted {
|
||||
if item.CurrentVersionID == "" || item.FileCount != 1 {
|
||||
t.Fatalf("skill %s has no initialized package", item.ID)
|
||||
}
|
||||
assertSkillVersionCount(t, db, item.ID, 1)
|
||||
version, err := svc.repo.SkillVersion(item.CurrentVersionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := svc.readSkillArchiveEntry(version, "SKILL.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != item.Instruction {
|
||||
t.Fatalf("skill %s package instruction was changed", item.ID)
|
||||
}
|
||||
}
|
||||
list, err := svc.Skills("test-user", SkillListRequest{Scope: "public", Search: "freestyle-template-match"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if list.TotalCount != 1 || len(list.Skills) != 1 || list.Skills[0].SkillID != "16000000000003" {
|
||||
t.Fatalf("community skill not visible in public search: %#v", list)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user