Vectrum
Sign In
121 lines · 3.8 KB · Go
package tools

import (
	"encoding/json"
	"strings"
	"testing"

	"harness/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)
	}
}