返回 DeepSeek-Reasonix
command_test.go
根目录 / internal / command / command_test.go
1 package command
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 fileencoding "reasonix/internal/fileutil/encoding"
9 )
10
11 func TestRender(t *testing.T) {
12 c := Command{Body: "Review $1 focusing on $ARGUMENTS. Cost: $$5. Missing: [$3]"}
13 got := c.Render([]string{"main.go", "bugs"})
14 want := "Review main.go focusing on main.go bugs. Cost: $5. Missing: []"
15 if got != want {
16 t.Errorf("Render = %q, want %q", got, want)
17 }
18
19 // No args: $ARGUMENTS and $N collapse to empty.
20 if got := (Command{Body: "x=$ARGUMENTS y=$1"}).Render(nil); got != "x= y=" {
21 t.Errorf("empty-args Render = %q", got)
22 }
23 }
24
25 func write(t *testing.T, dir, rel, content string) {
26 t.Helper()
27 p := filepath.Join(dir, rel)
28 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
29 t.Fatal(err)
30 }
31 if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
32 t.Fatal(err)
33 }
34 }
35
36 func TestLoad(t *testing.T) {
37 dir := t.TempDir()
38 write(t, dir, "review.md", "---\ndescription: Review the diff\nargument-hint: [area]\n---\nReview, focus on $ARGUMENTS.")
39 write(t, dir, "plain.md", "No frontmatter, just $1.")
40 write(t, dir, "git/commit.md", "---\ndescription: Commit\n---\nWrite a commit message.")
41 write(t, dir, "notes.txt", "ignored — not markdown")
42
43 cmds, err := Load(dir)
44 if err != nil {
45 t.Fatalf("Load: %v", err)
46 }
47 if len(cmds) != 3 {
48 t.Fatalf("loaded %d commands, want 3 (%v)", len(cmds), names(cmds))
49 }
50
51 byName := map[string]Command{}
52 for _, c := range cmds {
53 byName[c.Name] = c
54 }
55
56 r, ok := byName["review"]
57 if !ok || r.Description != "Review the diff" || r.ArgHint != "[area]" {
58 t.Errorf("review parsed wrong: %+v", r)
59 }
60 if r.Body != "Review, focus on $ARGUMENTS." {
61 t.Errorf("review body = %q", r.Body)
62 }
63 if p := byName["plain"]; p.Body != "No frontmatter, just $1." || p.Description != "" {
64 t.Errorf("plain parsed wrong: %+v", p)
65 }
66 if _, ok := byName["git:commit"]; !ok {
67 t.Errorf("subdir namespacing failed: %v", names(cmds))
68 }
69 }
70
71 func TestLoadDecodesGB18030CommandFile(t *testing.T) {
72 dir := t.TempDir()
73 body := "---\ndescription: 中文命令\nargument-hint: [主题]\n---\n请总结 $ARGUMENTS。"
74 path := filepath.Join(dir, "summary.md")
75 if err := os.WriteFile(path, fileencoding.Encode(body, fileencoding.GB18030), 0o644); err != nil {
76 t.Fatal(err)
77 }
78
79 cmds, err := Load(dir)
80 if err != nil {
81 t.Fatalf("Load: %v", err)
82 }
83 if len(cmds) != 1 || cmds[0].Description != "中文命令" || cmds[0].ArgHint != "[主题]" || cmds[0].Body != "请总结 $ARGUMENTS。" {
84 t.Fatalf("decoded command = %+v", cmds)
85 }
86 }
87
88 func TestLoadOverrideAndMissingDir(t *testing.T) {
89 user := t.TempDir()
90 project := t.TempDir()
91 write(t, user, "review.md", "USER version")
92 write(t, project, "review.md", "PROJECT version")
93
94 // Later dir (project) wins on a name clash; a non-existent dir is skipped.
95 cmds, err := Load("/no/such/dir", user, project)
96 if err != nil {
97 t.Fatalf("Load: %v", err)
98 }
99 if len(cmds) != 1 || cmds[0].Body != "PROJECT version" {
100 t.Errorf("override failed: %+v", cmds)
101 }
102 }
103
104 func TestLoadRootsUsesCanonicalPluginNamesAndHiddenCompatibleShortName(t *testing.T) {
105 pluginDir := t.TempDir()
106 projectDir := t.TempDir()
107 write(t, pluginDir, "plan.md", "---\ndescription: Plugin plan\n---\nPLUGIN $ARGUMENTS")
108 write(t, pluginDir, "status.md", "PLUGIN STATUS")
109 write(t, projectDir, "plan.md", "---\ndescription: Project plan\n---\nPROJECT $ARGUMENTS")
110
111 cmds, err := LoadRoots(
112 Root{Path: pluginDir, Plugin: "planning-with-files"},
113 Root{Path: projectDir},
114 )
115 if err != nil {
116 t.Fatalf("LoadRoots: %v", err)
117 }
118 byName := map[string]Command{}
119 for _, cmd := range cmds {
120 byName[cmd.Name] = cmd
121 }
122 if got := byName["plan"]; got.Body != "PROJECT $ARGUMENTS" || got.Plugin != "" || got.ShortName != "" || got.Hidden {
123 t.Fatalf("short-name winner = %+v, want project command", got)
124 }
125 canonical, ok := byName["planning-with-files:plan"]
126 if !ok || canonical.Body != "PLUGIN $ARGUMENTS" || canonical.Plugin != "planning-with-files" || canonical.ShortName != "plan" || canonical.Hidden {
127 t.Fatalf("canonical plugin command = %+v, %v", canonical, ok)
128 }
129 if got := byName["status"]; got.Plugin != "planning-with-files" || got.ShortName != "status" || !got.Hidden {
130 t.Fatalf("short compatibility command = %+v, want hidden plugin alias", got)
131 }
132 if got := byName["planning-with-files:status"]; got.Plugin != "planning-with-files" || got.ShortName != "status" || got.Hidden {
133 t.Fatalf("canonical status command = %+v", got)
134 }
135 if got := canonical.Render([]string{"feature"}); got != "PLUGIN feature" {
136 t.Fatalf("canonical command render = %q", got)
137 }
138 }
139
140 func TestLoadRootsDoesNotReplaceAnExplicitQualifiedCommand(t *testing.T) {
141 pluginDir := t.TempDir()
142 projectDir := t.TempDir()
143 write(t, pluginDir, "plan.md", "PLUGIN")
144 write(t, projectDir, "plan.md", "PROJECT")
145 write(t, projectDir, "planning-with-files/plan.md", "EXPLICIT QUALIFIED")
146
147 cmds, err := LoadRoots(
148 Root{Path: pluginDir, Plugin: "planning-with-files"},
149 Root{Path: projectDir},
150 )
151 if err != nil {
152 t.Fatalf("LoadRoots: %v", err)
153 }
154 for _, cmd := range cmds {
155 if cmd.Name == "planning-with-files:plan" {
156 if cmd.Body != "EXPLICIT QUALIFIED" || cmd.Plugin != "" || cmd.ShortName != "" || cmd.Hidden {
157 t.Fatalf("explicit qualified command was replaced: %+v", cmd)
158 }
159 return
160 }
161 }
162 t.Fatal("explicit qualified command missing")
163 }
164
165 func TestLoadRootsOmitsAmbiguousShortPluginName(t *testing.T) {
166 alpha := t.TempDir()
167 beta := t.TempDir()
168 write(t, alpha, "plan.md", "ALPHA")
169 write(t, beta, "plan.md", "BETA")
170 cmds, err := LoadRoots(Root{Path: alpha, Plugin: "alpha"}, Root{Path: beta, Plugin: "beta"})
171 if err != nil {
172 t.Fatal(err)
173 }
174 byName := map[string]Command{}
175 for _, cmd := range cmds {
176 byName[cmd.Name] = cmd
177 }
178 if _, ok := byName["plan"]; ok {
179 t.Fatal("ambiguous plugin short name must not remain invocable")
180 }
181 if byName["alpha:plan"].Body != "ALPHA" || byName["beta:plan"].Body != "BETA" {
182 t.Fatalf("canonical plugin commands = %+v", cmds)
183 }
184 }
185
186 func names(cmds []Command) []string {
187 out := make([]string, len(cmds))
188 for i, c := range cmds {
189 out[i] = c.Name
190 }
191 return out
192 }
193
193 lines GO