Files
majordomo/skill/skill_test.go
steve 76ecf0e49e feat: skills — additive instruction+tool bundles, clock + calc examples
Phase 6: skill.New constructor satisfying the agent.Skill contract;
instruction-only skills; ordered additive composition; skill/clock
(injectable-clock time tools) and skill/calc (recursive-descent arithmetic
evaluator) as ready-made examples with full test suites incl. an
agent-loop round trip. ADR-0013; README skills section + matrix synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:13:07 +02:00

98 lines
2.8 KiB
Go

package skill_test
import (
"context"
"encoding/json"
"strings"
"testing"
"gitea.stevedudenhoeffer.com/steve/majordomo/agent"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
"gitea.stevedudenhoeffer.com/steve/majordomo/skill"
)
// The skill package must satisfy the agent contract.
var _ agent.Skill = (*skill.Skill)(nil)
func TestSkillConstruction(t *testing.T) {
tool := llm.Tool{Name: "t1"}
sk := skill.New("research",
skill.WithInstructions("Cite sources."),
skill.WithTools(tool),
)
if sk.Name() != "research" || sk.Instructions() != "Cite sources." {
t.Errorf("skill = %q / %q", sk.Name(), sk.Instructions())
}
if sk.Tools() == nil || len(sk.Tools().Tools()) != 1 {
t.Errorf("tools = %+v", sk.Tools())
}
if sk.Tools().Name() != "research" {
t.Errorf("toolbox name = %q, want skill name", sk.Tools().Name())
}
}
func TestInstructionOnlySkill(t *testing.T) {
sk := skill.New("tone", skill.WithInstructions("Be kind."))
if sk.Tools() != nil {
t.Error("instruction-only skill must have nil toolbox")
}
}
// TestSkillsAttachToAnyAgent: the same skill instance layers onto multiple
// agents, on demand, extending prompt and toolset additively.
func TestSkillsAttachToAnyAgent(t *testing.T) {
echo := llm.Tool{
Name: "echo",
Handler: func(_ context.Context, args json.RawMessage) (any, error) {
return string(args), nil
},
}
sk := skill.New("echoer",
skill.WithInstructions("Echo when asked."),
skill.WithTools(echo),
)
for _, base := range []string{"Agent one.", "Agent two."} {
fp := fake.New("fp")
fp.Enqueue("m", fake.Reply("done"))
m, _ := fp.Model("m")
a := agent.New(m, base)
a.AddSkill(sk)
if _, err := a.Run(context.Background(), "hi"); err != nil {
t.Fatalf("Run: %v", err)
}
req := fp.Calls()[0].Request
if !strings.HasPrefix(req.System, base) || !strings.Contains(req.System, "Echo when asked.") {
t.Errorf("system = %q", req.System)
}
if len(req.Tools) != 1 || req.Tools[0].Name != "echo" {
t.Errorf("tools = %+v", req.Tools)
}
}
}
// TestMultipleSkillsCompose: instructions append in order; toolsets merge.
func TestMultipleSkillsCompose(t *testing.T) {
fp := fake.New("fp")
fp.Enqueue("m", fake.Reply("ok"))
m, _ := fp.Model("m")
a := agent.New(m, "Base.",
agent.WithSkill(skill.New("one", skill.WithInstructions("First."), skill.WithTools(llm.Tool{Name: "t1"}))),
agent.WithSkill(skill.New("two", skill.WithInstructions("Second."), skill.WithTools(llm.Tool{Name: "t2"}))),
)
if _, err := a.Run(context.Background(), "go"); err != nil {
t.Fatalf("Run: %v", err)
}
req := fp.Calls()[0].Request
if req.System != "Base.\n\nFirst.\n\nSecond." {
t.Errorf("system = %q", req.System)
}
if len(req.Tools) != 2 {
t.Errorf("tools = %+v", req.Tools)
}
}