返回 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/skill"
10 "reasonix/internal/tool"
11 )
12
13 type capabilityRecordingRunner struct {
14 input string
15 }
16
17 func TestLegacySkillProfilesDoNotFilterCapabilityRoutes(t *testing.T) {
18 // Skill profiles frontmatter is diagnostic-only: capability routing must
19 // surface every trigger match. The single adaptive standard execution shares
20 // one catalog, so legacy economy/balanced labels never gate availability.
21 runner := &capabilityRecordingRunner{}
22 reg := tool.NewRegistry()
23 reg.Add(capabilityTestTool{name: "run_skill"})
24 c := newOwnedTestController(t, Options{
25 Runner: runner,
26 Skills: []skill.Skill{
27 {Name: "economy-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"economy"}},
28 {Name: "balanced-review", Description: "review code", Triggers: []string{"review code"}, Profiles: []string{"balanced"}},
29 },
30 Registry: reg,
31 })
32
33 if err := c.Run(context.Background(), "review code"); err != nil {
34 t.Fatalf("Run: %v", err)
35 }
36 if !strings.Contains(runner.input, "skill:economy-review prefer") {
37 t.Fatalf("economy skill missing from route:\n%s", runner.input)
38 }
39 if !strings.Contains(runner.input, "skill:balanced-review prefer") {
40 t.Fatalf("balanced skill should remain routable despite legacy profile labels:\n%s", runner.input)
41 }
42 }
43
44 func (r *capabilityRecordingRunner) Run(_ context.Context, input string) error {
45 r.input = input
46 return nil
47 }
48
49 type capabilityTestTool struct{ name string }
50
51 func (t capabilityTestTool) Name() string { return t.name }
52 func (t capabilityTestTool) Description() string {
53 return "test tool"
54 }
55 func (t capabilityTestTool) Schema() json.RawMessage {
56 return json.RawMessage(`{"type":"object","properties":{}}`)
57 }
58 func (t capabilityTestTool) Execute(context.Context, json.RawMessage) (string, error) {
59 return "ok", nil
60 }
61 func (t capabilityTestTool) ReadOnly() bool { return true }
62
63 func TestRunInjectsCapabilityRouteForRelevantSkill(t *testing.T) {
64 runner := &capabilityRecordingRunner{}
65 reg := tool.NewRegistry()
66 reg.Add(capabilityTestTool{name: "run_skill"})
67 c := newOwnedTestController(t, Options{
68 Runner: runner,
69 Skills: []skill.Skill{{
70 Name: "review",
71 Description: "review code",
72 Scope: skill.ScopeBuiltin,
73 }},
74 Registry: reg,
75 })
76
77 if err := c.Run(context.Background(), "帮我看看这段代码有没有问题"); err != nil {
78 t.Fatalf("Run: %v", err)
79 }
80 if !strings.Contains(runner.input, `<capability-route version="1">`) ||
81 !strings.Contains(runner.input, "skill:review prefer") {
82 t.Fatalf("input missing capability route:\n%s", runner.input)
83 }
84 if got := StripComposePrefixes(runner.input); got != "帮我看看这段代码有没有问题" {
85 t.Fatalf("StripComposePrefixes = %q", got)
86 }
87 }
88
89 func TestSkillRouteUsesProxyWhenSkillToolsHiddenFromProviderSurface(t *testing.T) {
90 // Production hides run_skill from the provider-visible surface; the
91 // capability catalog must still see it in the registry, so the skill routes
92 // as ready via the concrete id instead of the retired connect_tool_source.
93 runner := &capabilityRecordingRunner{}
94 reg := tool.NewRegistry()
95 reg.Add(capabilityTestTool{name: "run_skill"})
96 reg.Add(capabilityTestTool{name: "use_capability"})
97 reg.SetProviderVisibleTools([]string{"use_capability"})
98 c := newOwnedTestController(t, Options{
99 Runner: runner,
100 Skills: []skill.Skill{{
101 Name: "review",
102 Description: "review code",
103 Scope: skill.ScopeBuiltin,
104 }},
105 Registry: reg,
106 })
107
108 if err := c.Run(context.Background(), "帮我看看这段代码有没有问题"); err != nil {
109 t.Fatalf("Run: %v", err)
110 }
111 if !strings.Contains(runner.input, "skill:review prefer") {
112 t.Fatalf("hidden skill tool should still route as ready:\n%s", runner.input)
113 }
114 if strings.Contains(runner.input, "connect_tool_source") || strings.Contains(runner.input, "source:skills") {
115 t.Fatalf("route emitted the retired skill connector:\n%s", runner.input)
116 }
117 }
118
119 func TestCreateSkillWritesThroughAndIsImmediatelyReadable(t *testing.T) {
120 home := t.TempDir()
121 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
122 c := newOwnedTestController(t, Options{AllSkillStore: st, SkillStore: st})
123
124 content := skill.RenderSkillFile(skill.SkillFileOptions{
125 Name: "helper", Description: "a helper", Body: "be helpful",
126 RunAs: skill.RunSubagent, Invocation: "manual",
127 })
128 path, err := c.CreateSkill("helper", skill.ScopeGlobal, content)
129 if err != nil {
130 t.Fatalf("CreateSkill: %v", err)
131 }
132 if path == "" {
133 t.Fatal("CreateSkill returned empty path")
134 }
135
136 // No rebuild — the live store re-scans on every call.
137 sk, found := c.RunSkill("/helper do a thing")
138 if !found {
139 t.Fatal("newly created skill should be immediately invocable by name")
140 }
141 if !strings.Contains(sk, "be helpful") {
142 t.Fatalf("rendered skill body missing from RunSkill output: %s", sk)
143 }
144 }
145
146 func TestCreateSkillRefusesWithoutWritableStore(t *testing.T) {
147 c := newOwnedTestController(t, Options{Skills: []skill.Skill{}, AllSkills: []skill.Skill{}})
148 if _, err := c.CreateSkill("x", skill.ScopeGlobal, "---\ndescription: x\n---\nbody"); err == nil {
149 t.Error("CreateSkill without a writable store should error")
150 }
151 if err := c.DeleteSkill("x", skill.ScopeGlobal); err == nil {
152 t.Error("DeleteSkill without a writable store should error")
153 }
154 }
155
156 func TestUpdateSkillOverwritesAndIsImmediatelyReadable(t *testing.T) {
157 home := t.TempDir()
158 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
159 c := newOwnedTestController(t, Options{AllSkillStore: st, SkillStore: st})
160
161 if _, err := c.CreateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
162 Name: "helper", Description: "v1", Body: "old", RunAs: skill.RunSubagent, Invocation: "manual",
163 })); err != nil {
164 t.Fatalf("CreateSkill: %v", err)
165 }
166 if err := c.UpdateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
167 Name: "helper", Description: "v2", Body: "new", RunAs: skill.RunSubagent, Invocation: "manual",
168 })); err != nil {
169 t.Fatalf("UpdateSkill: %v", err)
170 }
171 for _, sk := range c.AllSkills() {
172 if sk.Name == "helper" {
173 if sk.Description != "v2" || sk.Body != "" {
174 t.Fatalf("update did not take effect: description=%q body=%q", sk.Description, sk.Body)
175 }
176 loaded, ok := st.Read("helper")
177 if !ok || loaded.Body != "new" {
178 t.Fatalf("selected skill body = %q, ok=%v", loaded.Body, ok)
179 }
180 return
181 }
182 }
183 t.Fatal("helper missing from AllSkills after update")
184 }
185
186 func TestDeleteSkillRemovesLiveEntry(t *testing.T) {
187 home := t.TempDir()
188 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
189 c := newOwnedTestController(t, Options{AllSkillStore: st, SkillStore: st})
190
191 content := skill.RenderSkillFile(skill.SkillFileOptions{Name: "temp", Description: "temp", Body: "b"})
192 if _, err := c.CreateSkill("temp", skill.ScopeGlobal, content); err != nil {
193 t.Fatalf("CreateSkill: %v", err)
194 }
195 if err := c.DeleteSkill("temp", skill.ScopeGlobal); err != nil {
196 t.Fatalf("DeleteSkill: %v", err)
197 }
198 for _, sk := range c.AllSkills() {
199 if sk.Name == "temp" {
200 t.Fatal("deleted skill still present in AllSkills")
201 }
202 }
203 }
204
204 lines GO