返回 DeepSeek-Reasonix
capability_test.go
根目录 / internal / control / capability_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "reasonix/internal/capability"
10 "reasonix/internal/skill"
11 "reasonix/internal/tool"
12 )
13
14 type capabilityRecordingRunner struct {
15 input string
16 }
17
18 func TestEconomyRoutesOnlyEconomyEligibleSkills(t *testing.T) {
19 runner := &capabilityRecordingRunner{}
20 reg := tool.NewRegistry()
21 reg.Add(capabilityTestTool{name: "run_skill"})
22 c := New(Options{
23 Runner: runner,
24 Skills: []skill.Skill{
25 {Name: "economy-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"economy"}},
26 {Name: "balanced-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"balanced"}},
27 },
28 Registry: reg,
29 RuntimeProfile: capability.ProfileEconomy,
30 })
31
32 if err := c.Run(context.Background(), "review code"); err != nil {
33 t.Fatalf("Run: %v", err)
34 }
35 if !strings.Contains(runner.input, "skill:economy-review prefer") {
36 t.Fatalf("economy skill missing from route:\n%s", runner.input)
37 }
38 if strings.Contains(runner.input, "skill:balanced-review") {
39 t.Fatalf("balanced-only skill leaked into economy route:\n%s", runner.input)
40 }
41 }
42
43 func (r *capabilityRecordingRunner) Run(_ context.Context, input string) error {
44 r.input = input
45 return nil
46 }
47
48 type capabilityTestTool struct{ name string }
49
50 func (t capabilityTestTool) Name() string { return t.name }
51 func (t capabilityTestTool) Description() string {
52 return "test tool"
53 }
54 func (t capabilityTestTool) Schema() json.RawMessage {
55 return json.RawMessage(`{"type":"object","properties":{}}`)
56 }
57 func (t capabilityTestTool) Execute(context.Context, json.RawMessage) (string, error) {
58 return "ok", nil
59 }
60 func (t capabilityTestTool) ReadOnly() bool { return true }
61
62 func TestRunInjectsCapabilityRouteForRelevantSkill(t *testing.T) {
63 runner := &capabilityRecordingRunner{}
64 reg := tool.NewRegistry()
65 reg.Add(capabilityTestTool{name: "run_skill"})
66 c := New(Options{
67 Runner: runner,
68 Skills: []skill.Skill{{
69 Name: "review",
70 Description: "review code",
71 Scope: skill.ScopeBuiltin,
72 }},
73 Registry: reg,
74 })
75
76 if err := c.Run(context.Background(), "帮我看看这段代码有没有问题"); err != nil {
77 t.Fatalf("Run: %v", err)
78 }
79 if !strings.Contains(runner.input, `<capability-route version="1">`) ||
80 !strings.Contains(runner.input, "skill:review prefer") {
81 t.Fatalf("input missing capability route:\n%s", runner.input)
82 }
83 if got := StripComposePrefixes(runner.input); got != "帮我看看这段代码有没有问题" {
84 t.Fatalf("StripComposePrefixes = %q", got)
85 }
86 }
87
88 func TestCreateSkillWritesThroughAndIsImmediatelyReadable(t *testing.T) {
89 home := t.TempDir()
90 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
91 c := New(Options{AllSkillStore: st, SkillStore: st})
92
93 content := skill.RenderSkillFile(skill.SkillFileOptions{
94 Name: "helper", Description: "a helper", Body: "be helpful",
95 RunAs: skill.RunSubagent, Invocation: "manual",
96 })
97 path, err := c.CreateSkill("helper", skill.ScopeGlobal, content)
98 if err != nil {
99 t.Fatalf("CreateSkill: %v", err)
100 }
101 if path == "" {
102 t.Fatal("CreateSkill returned empty path")
103 }
104
105 // No rebuild — the live store re-scans on every call.
106 sk, found := c.RunSkill("/helper do a thing")
107 if !found {
108 t.Fatal("newly created skill should be immediately invocable by name")
109 }
110 if !strings.Contains(sk, "be helpful") {
111 t.Fatalf("rendered skill body missing from RunSkill output: %s", sk)
112 }
113 }
114
115 func TestCreateSkillRefusesWithoutWritableStore(t *testing.T) {
116 c := New(Options{Skills: []skill.Skill{}, AllSkills: []skill.Skill{}})
117 if _, err := c.CreateSkill("x", skill.ScopeGlobal, "---\ndescription: x\n---\nbody"); err == nil {
118 t.Error("CreateSkill without a writable store should error")
119 }
120 if err := c.DeleteSkill("x", skill.ScopeGlobal); err == nil {
121 t.Error("DeleteSkill without a writable store should error")
122 }
123 }
124
125 func TestUpdateSkillOverwritesAndIsImmediatelyReadable(t *testing.T) {
126 home := t.TempDir()
127 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
128 c := New(Options{AllSkillStore: st, SkillStore: st})
129
130 if _, err := c.CreateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
131 Name: "helper", Description: "v1", Body: "old", RunAs: skill.RunSubagent, Invocation: "manual",
132 })); err != nil {
133 t.Fatalf("CreateSkill: %v", err)
134 }
135 if err := c.UpdateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
136 Name: "helper", Description: "v2", Body: "new", RunAs: skill.RunSubagent, Invocation: "manual",
137 })); err != nil {
138 t.Fatalf("UpdateSkill: %v", err)
139 }
140 for _, sk := range c.AllSkills() {
141 if sk.Name == "helper" {
142 if sk.Description != "v2" || sk.Body != "new" {
143 t.Fatalf("update did not take effect: description=%q body=%q", sk.Description, sk.Body)
144 }
145 return
146 }
147 }
148 t.Fatal("helper missing from AllSkills after update")
149 }
150
151 func TestDeleteSkillRemovesLiveEntry(t *testing.T) {
152 home := t.TempDir()
153 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
154 c := New(Options{AllSkillStore: st, SkillStore: st})
155
156 content := skill.RenderSkillFile(skill.SkillFileOptions{Name: "temp", Description: "temp", Body: "b"})
157 if _, err := c.CreateSkill("temp", skill.ScopeGlobal, content); err != nil {
158 t.Fatalf("CreateSkill: %v", err)
159 }
160 if err := c.DeleteSkill("temp", skill.ScopeGlobal); err != nil {
161 t.Fatalf("DeleteSkill: %v", err)
162 }
163 for _, sk := range c.AllSkills() {
164 if sk.Name == "temp" {
165 t.Fatal("deleted skill still present in AllSkills")
166 }
167 }
168 }
169
169 lines GO