169 lines
5.5 KiB
Go
169 lines
5.5 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/capability"
|
|
"reasonix/internal/skill"
|
|
"reasonix/internal/tool"
|
|
)
|
|
|
|
type capabilityRecordingRunner struct {
|
|
input string
|
|
}
|
|
|
|
func TestEconomyRoutesOnlyEconomyEligibleSkills(t *testing.T) {
|
|
runner := &capabilityRecordingRunner{}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(capabilityTestTool{name: "run_skill"})
|
|
c := New(Options{
|
|
Runner: runner,
|
|
Skills: []skill.Skill{
|
|
{Name: "economy-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"economy"}},
|
|
{Name: "balanced-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"balanced"}},
|
|
},
|
|
Registry: reg,
|
|
RuntimeProfile: capability.ProfileEconomy,
|
|
})
|
|
|
|
if err := c.Run(context.Background(), "review code"); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if !strings.Contains(runner.input, "skill:economy-review prefer") {
|
|
t.Fatalf("economy skill missing from route:\n%s", runner.input)
|
|
}
|
|
if strings.Contains(runner.input, "skill:balanced-review") {
|
|
t.Fatalf("balanced-only skill leaked into economy route:\n%s", runner.input)
|
|
}
|
|
}
|
|
|
|
func (r *capabilityRecordingRunner) Run(_ context.Context, input string) error {
|
|
r.input = input
|
|
return nil
|
|
}
|
|
|
|
type capabilityTestTool struct{ name string }
|
|
|
|
func (t capabilityTestTool) Name() string { return t.name }
|
|
func (t capabilityTestTool) Description() string {
|
|
return "test tool"
|
|
}
|
|
func (t capabilityTestTool) Schema() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object","properties":{}}`)
|
|
}
|
|
func (t capabilityTestTool) Execute(context.Context, json.RawMessage) (string, error) {
|
|
return "ok", nil
|
|
}
|
|
func (t capabilityTestTool) ReadOnly() bool { return true }
|
|
|
|
func TestRunInjectsCapabilityRouteForRelevantSkill(t *testing.T) {
|
|
runner := &capabilityRecordingRunner{}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(capabilityTestTool{name: "run_skill"})
|
|
c := New(Options{
|
|
Runner: runner,
|
|
Skills: []skill.Skill{{
|
|
Name: "review",
|
|
Description: "review code",
|
|
Scope: skill.ScopeBuiltin,
|
|
}},
|
|
Registry: reg,
|
|
})
|
|
|
|
if err := c.Run(context.Background(), "帮我看看这段代码有没有问题"); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if !strings.Contains(runner.input, `<capability-route version="1">`) ||
|
|
!strings.Contains(runner.input, "skill:review prefer") {
|
|
t.Fatalf("input missing capability route:\n%s", runner.input)
|
|
}
|
|
if got := StripComposePrefixes(runner.input); got != "帮我看看这段代码有没有问题" {
|
|
t.Fatalf("StripComposePrefixes = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCreateSkillWritesThroughAndIsImmediatelyReadable(t *testing.T) {
|
|
home := t.TempDir()
|
|
st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
|
|
c := New(Options{AllSkillStore: st, SkillStore: st})
|
|
|
|
content := skill.RenderSkillFile(skill.SkillFileOptions{
|
|
Name: "helper", Description: "a helper", Body: "be helpful",
|
|
RunAs: skill.RunSubagent, Invocation: "manual",
|
|
})
|
|
path, err := c.CreateSkill("helper", skill.ScopeGlobal, content)
|
|
if err != nil {
|
|
t.Fatalf("CreateSkill: %v", err)
|
|
}
|
|
if path == "" {
|
|
t.Fatal("CreateSkill returned empty path")
|
|
}
|
|
|
|
// No rebuild — the live store re-scans on every call.
|
|
sk, found := c.RunSkill("/helper do a thing")
|
|
if !found {
|
|
t.Fatal("newly created skill should be immediately invocable by name")
|
|
}
|
|
if !strings.Contains(sk, "be helpful") {
|
|
t.Fatalf("rendered skill body missing from RunSkill output: %s", sk)
|
|
}
|
|
}
|
|
|
|
func TestCreateSkillRefusesWithoutWritableStore(t *testing.T) {
|
|
c := New(Options{Skills: []skill.Skill{}, AllSkills: []skill.Skill{}})
|
|
if _, err := c.CreateSkill("x", skill.ScopeGlobal, "---\ndescription: x\n---\nbody"); err == nil {
|
|
t.Error("CreateSkill without a writable store should error")
|
|
}
|
|
if err := c.DeleteSkill("x", skill.ScopeGlobal); err == nil {
|
|
t.Error("DeleteSkill without a writable store should error")
|
|
}
|
|
}
|
|
|
|
func TestUpdateSkillOverwritesAndIsImmediatelyReadable(t *testing.T) {
|
|
home := t.TempDir()
|
|
st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
|
|
c := New(Options{AllSkillStore: st, SkillStore: st})
|
|
|
|
if _, err := c.CreateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
|
|
Name: "helper", Description: "v1", Body: "old", RunAs: skill.RunSubagent, Invocation: "manual",
|
|
})); err != nil {
|
|
t.Fatalf("CreateSkill: %v", err)
|
|
}
|
|
if err := c.UpdateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
|
|
Name: "helper", Description: "v2", Body: "new", RunAs: skill.RunSubagent, Invocation: "manual",
|
|
})); err != nil {
|
|
t.Fatalf("UpdateSkill: %v", err)
|
|
}
|
|
for _, sk := range c.AllSkills() {
|
|
if sk.Name == "helper" {
|
|
if sk.Description != "v2" || sk.Body != "new" {
|
|
t.Fatalf("update did not take effect: description=%q body=%q", sk.Description, sk.Body)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("helper missing from AllSkills after update")
|
|
}
|
|
|
|
func TestDeleteSkillRemovesLiveEntry(t *testing.T) {
|
|
home := t.TempDir()
|
|
st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
|
|
c := New(Options{AllSkillStore: st, SkillStore: st})
|
|
|
|
content := skill.RenderSkillFile(skill.SkillFileOptions{Name: "temp", Description: "temp", Body: "b"})
|
|
if _, err := c.CreateSkill("temp", skill.ScopeGlobal, content); err != nil {
|
|
t.Fatalf("CreateSkill: %v", err)
|
|
}
|
|
if err := c.DeleteSkill("temp", skill.ScopeGlobal); err != nil {
|
|
t.Fatalf("DeleteSkill: %v", err)
|
|
}
|
|
for _, sk := range c.AllSkills() {
|
|
if sk.Name == "temp" {
|
|
t.Fatal("deleted skill still present in AllSkills")
|
|
}
|
|
}
|
|
}
|