package tools
import (
"encoding/json"
"strings"
"testing"
"github.com/johannSo/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)
}
}