package tools import ( "encoding/json" "strings" "testing" "codey/internal/skills" ) func testSkills() []skills.Skill { return []skills.Skill{ {Name: "alpha", Description: "the first one", Body: "do the alpha thing", Dir: "/skills/alpha", Source: "personal"}, {Name: "beta", Description: "the second one", Body: "do the beta thing", Dir: "/skills/beta", Source: "project"}, } } func TestSkillReturnsBodyWithDirectory(t *testing.T) { tool := NewSkill(testSkills()) out, err := run(t, tool, map[string]any{"name": "alpha"}) if err != nil { t.Fatal(err) } for _, want := range []string{"Skill: alpha", "Directory: /skills/alpha", "do the alpha thing"} { if !strings.Contains(out, want) { t.Errorf("output missing %q:\n%s", want, out) } } } func TestSkillAppendsArguments(t *testing.T) { tool := NewSkill(testSkills()) out, err := run(t, tool, map[string]any{ "name": "beta", "args": "fix the flaky test", }) if err != nil { t.Fatal(err) } if !strings.Contains(out, "ARGUMENTS: fix the flaky test") { t.Errorf("arguments missing:\n%s", out) } } func TestSkillOmitsArgumentsLineWhenEmpty(t *testing.T) { tool := NewSkill(testSkills()) out, _ := run(t, tool, map[string]any{"name": "alpha"}) if strings.Contains(out, "ARGUMENTS:") { t.Errorf("empty args produced an ARGUMENTS line:\n%s", out) } } func TestSkillUnknownNameListsTheRealOnes(t *testing.T) { tool := NewSkill(testSkills()) _, err := run(t, tool, map[string]any{"name": "nope"}) if err == nil { t.Fatal("unknown skill was accepted") } if !strings.Contains(err.Error(), "alpha") || !strings.Contains(err.Error(), "beta") { t.Errorf("error does not list the available skills: %v", err) } } func TestSkillSchemaEnumeratesSlugs(t *testing.T) { tool := NewSkill(testSkills()) var schema struct { Properties struct { Name struct { Enum []string `json:"enum"` } `json:"name"` } `json:"properties"` } if err := json.Unmarshal(tool.InputSchema(), &schema); err != nil { t.Fatal(err) } if strings.Join(schema.Properties.Name.Enum, ",") != "alpha,beta" { t.Errorf("enum = %v, want [alpha beta]", schema.Properties.Name.Enum) } } func TestSkillDescriptionListsSkills(t *testing.T) { tool := NewSkill(testSkills()) d := tool.Description() if !strings.Contains(d, "alpha: the first one") || !strings.Contains(d, "beta: the second one") { t.Errorf("tool description does not list the skills:\n%s", d) } } func TestNewSkillWithNoneIsNil(t *testing.T) { if NewSkill(nil) != nil { t.Error("NewSkill(nil) must return nil so the caller registers nothing") } } func TestSkillRepeatInvocationReturnsBodyAgain(t *testing.T) { // Deduping would save tokens and cost the model the ability to re-read // instructions that compaction dropped. tool := NewSkill(testSkills()) first, _ := run(t, tool, map[string]any{"name": "alpha"}) second, _ := run(t, tool, map[string]any{"name": "alpha"}) if first != second || first == "" { t.Error("the second invocation did not return the same body") } } // The preamble tells the model how to reach a skill's bundled files. It has to // be true: read_file resolves a relative path against the working root, not // against the skill directory, so an instruction to use relative paths sends // the model straight into a "no such file" it cannot diagnose. func TestSkillPreambleDoesNotPromiseRelativeResolution(t *testing.T) { tool := NewSkill(testSkills()) out, err := run(t, tool, map[string]any{"name": "alpha"}) if err != nil { t.Fatal(err) } if strings.Contains(out, "resolve against that directory") { t.Errorf("preamble still claims relative paths resolve against the skill directory:\n%s", out) } if !strings.Contains(out, "Join relative paths onto the directory above") { t.Errorf("preamble does not tell the model to join onto the directory:\n%s", out) } }