package agentspec import ( "os" "path/filepath" "strings" "testing" ) const goodSpec = `--- name: doc-reviewer description: 评审体系文档章节 tools: read_file, grep, write_file disallowedTools: run_shell model: lightweight skills: house-style, rdm-review maxTurns: 8 memoryScope: project effort: high background: true worktreeIsolation: false outputSchema: review-result --- 你是章节评审代理。逐条核对来源标注。 ` func TestParseFullFields(t *testing.T) { spec, err := Parse(goodSpec) if err != nil { t.Fatal(err) } if spec.Name != "doc-reviewer" || spec.Description == "" { t.Fatalf("identity = %+v", spec) } if len(spec.Tools) != 3 || spec.Tools[2] != "write_file" { t.Fatalf("tools = %v", spec.Tools) } if len(spec.DisallowedTools) != 1 || spec.DisallowedTools[0] != "run_shell" { t.Fatalf("disallowed = %v", spec.DisallowedTools) } if spec.Model != "lightweight" || spec.MaxTurns != 8 || spec.MemoryScope != "project" || spec.Effort != "high" { t.Fatalf("scalars = %+v", spec) } if !spec.Background || spec.WorktreeIsolation { t.Fatalf("bools = %+v", spec) } if len(spec.Skills) != 2 || spec.OutputSchema != "review-result" { t.Fatalf("skills/schema = %+v", spec) } if spec.Prompt == "" { t.Fatal("prompt body empty") } } func TestParseDefaultsAndMinimal(t *testing.T) { spec, err := Parse("---\nname: helper\n---\n做杂事。\n") if err != nil { t.Fatal(err) } if spec.MaxTurns != 0 || spec.Background || len(spec.Tools) != 0 || spec.PermissionMode != "" { t.Fatalf("defaults = %+v", spec) } if spec.Prompt != "做杂事。" { t.Fatalf("prompt = %q", spec.Prompt) } } func TestParseRejectsBadInputs(t *testing.T) { cases := map[string]string{ "missing frontmatter": "name: x\n---\n正文\n", "unclosed frontmatter": "---\nname: x\n正文\n", "unknown key": "---\nname: x\nbogus: 1\n---\n正文\n", "missing name": "---\ndescription: x\n---\n正文\n", "bad name uppercase": "---\nname: Bad\n---\n正文\n", "bad name underscore": "---\nname: has_under\n---\n正文\n", "bad maxTurns": "---\nname: x\nmaxTurns: many\n---\n正文\n", "bad bool": "---\nname: x\nbackground: yes\n---\n正文\n", "malformed line": "---\nname: x\nno-colon-here\n---\n正文\n", } for label, content := range cases { if _, err := Parse(content); err == nil { t.Fatalf("%s: expected error", label) } } } func TestParsePluginBansHostOnlyKeys(t *testing.T) { for _, key := range []string{"hooks", "mcp", "permissionMode"} { content := "---\nname: plug-agent\n" + key + ": anything\n---\n正文\n" if _, err := ParsePlugin(content); err == nil { t.Fatalf("plugin key %s: expected rejection", key) } else if got := err.Error(); !strings.Contains(got, "不得声明 "+key) { t.Fatalf("plugin key %s: error %q 未定位到字段", key, got) } } // 宿主来源不受此限:permissionMode 在 Parse 下可用。 if _, err := Parse("---\nname: host-agent\npermissionMode: plan\n---\n正文\n"); err != nil { t.Fatalf("host parse with permissionMode: %v", err) } // 插件的合法字段照常解析。 spec, err := ParsePlugin("---\nname: plug-agent\ntools: read_file\n---\n正文\n") if err != nil || len(spec.Tools) != 1 { t.Fatalf("plugin good spec: %+v err=%v", spec, err) } } func TestLoadDirAtomicAndDuplicate(t *testing.T) { dir := t.TempDir() write := func(name, content string) { if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatal(err) } } write("a.md", "---\nname: alpha\n---\nA\n") write("b.md", "---\nname: beta\n---\nB\n") got, err := LoadDir(dir, nil) if err != nil || len(got) != 2 { t.Fatalf("load = %d err=%v", len(got), err) } write("c.md", "---\nname: alpha\n---\nC\n") if _, err := LoadDir(dir, nil); err == nil { t.Fatal("duplicate name accepted") } os.Remove(filepath.Join(dir, "c.md")) write("bad.md", "---\nname: x\nbogus: 1\n---\n") if _, err := LoadDir(dir, nil); err == nil { t.Fatal("bad file did not fail the batch") } }