返回 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 TestCreateSkillWritesThroughAndIsImmediatelyReadable(t *testing.T) {
90 home := t.TempDir()
91 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
92 c := newOwnedTestController(t, Options{AllSkillStore: st, SkillStore: st})
93
94 content := skill.RenderSkillFile(skill.SkillFileOptions{
95 Name: "helper", Description: "a helper", Body: "be helpful",
96 RunAs: skill.RunSubagent, Invocation: "manual",
97 })
98 path, err := c.CreateSkill("helper", skill.ScopeGlobal, content)
99 if err != nil {
100 t.Fatalf("CreateSkill: %v", err)
101 }
102 if path == "" {
103 t.Fatal("CreateSkill returned empty path")
104 }
105
106 // No rebuild — the live store re-scans on every call.
107 sk, found := c.RunSkill("/helper do a thing")
108 if !found {
109 t.Fatal("newly created skill should be immediately invocable by name")
110 }
111 if !strings.Contains(sk, "be helpful") {
112 t.Fatalf("rendered skill body missing from RunSkill output: %s", sk)
113 }
114 }
115
116 func TestCreateSkillRefusesWithoutWritableStore(t *testing.T) {
117 c := newOwnedTestController(t, Options{Skills: []skill.Skill{}, AllSkills: []skill.Skill{}})
118 if _, err := c.CreateSkill("x", skill.ScopeGlobal, "---\ndescription: x\n---\nbody"); err == nil {
119 t.Error("CreateSkill without a writable store should error")
120 }
121 if err := c.DeleteSkill("x", skill.ScopeGlobal); err == nil {
122 t.Error("DeleteSkill without a writable store should error")
123 }
124 }
125
126 func TestUpdateSkillOverwritesAndIsImmediatelyReadable(t *testing.T) {
127 home := t.TempDir()
128 st := skill.New(skill.Options{HomeDir: home, DisableBuiltins: true})
129 c := newOwnedTestController(t, Options{AllSkillStore: st, SkillStore: st})
130
131 if _, err := c.CreateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
132 Name: "helper", Description: "v1", Body: "old", RunAs: skill.RunSubagent, Invocation: "manual",
133 })); err != nil {
134 t.Fatalf("CreateSkill: %v", err)
135 }
136 if err := c.UpdateSkill("helper", skill.ScopeGlobal, skill.RenderSkillFile(skill.SkillFileOptions{
137 Name: "helper", Description: "v2", Body: "new", RunAs: skill.RunSubagent, Invocation: "manual",
138 })); err != nil {
139 t.Fatalf("UpdateSkill: %v", err)
140 }
141 for _, sk := range c.AllSkills() {
142 if sk.Name == "helper" {
143 if sk.Description != "v2" || sk.Body != "" {
144 t.Fatalf("update did not take effect: description=%q body=%q", sk.Description, sk.Body)
145 }
146 loaded, ok := st.Read("helper")
147 if !ok || loaded.Body != "new" {
148 t.Fatalf("selected skill body = %q, ok=%v", loaded.Body, ok)
149 }
150 return
151 }
152 }
153 t.Fatal("helper missing from AllSkills after update")
154 }
155
156 func TestDeleteSkillRemovesLiveEntry(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 content := skill.RenderSkillFile(skill.SkillFileOptions{Name: "temp", Description: "temp", Body: "b"})
162 if _, err := c.CreateSkill("temp", skill.ScopeGlobal, content); err != nil {
163 t.Fatalf("CreateSkill: %v", err)
164 }
165 if err := c.DeleteSkill("temp", skill.ScopeGlobal); err != nil {
166 t.Fatalf("DeleteSkill: %v", err)
167 }
168 for _, sk := range c.AllSkills() {
169 if sk.Name == "temp" {
170 t.Fatal("deleted skill still present in AllSkills")
171 }
172 }
173 }
174
174 lines GO