-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathselector_test.go
More file actions
355 lines (299 loc) · 13.4 KB
/
selector_test.go
File metadata and controls
355 lines (299 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package ophis
import (
"slices"
"testing"
"github.com/google/jsonschema-go/jsonschema"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// hasSchemaType checks if a schema has the specified type.
// In jsonschema-go v0.4.2+, nullable types use Types []string instead of Type string.
// For example, []string with omitempty becomes Types: ["null", "array"] instead of Type: "array".
func hasSchemaType(schema *jsonschema.Schema, expectedType string) bool {
if schema.Type == expectedType {
return true
}
return slices.Contains(schema.Types, expectedType)
}
// buildCommandTree creates a command tree from a list of command names.
// The first command becomes the root, and subsequent commands are nested.
func buildCommandTree(names ...string) *cobra.Command {
if len(names) == 0 {
return nil
}
root := &cobra.Command{Use: names[0]}
parent := root
for _, name := range names[1:] {
child := &cobra.Command{
Use: name,
Run: func(_ *cobra.Command, _ []string) {},
}
parent.AddCommand(child)
parent = child
}
return parent
}
type SomeJSONObject struct {
Foo string
Bar int
FooBar struct {
Baz string
}
}
type SomeJSONArray []SomeJSONObject
func TestCreateToolFromCmd(t *testing.T) {
// Create a simple test command
cmd := &cobra.Command{
Use: "test [file]",
Short: "Test command",
Long: "This is a test command for testing the ophis package",
Example: "test file.txt --output result.txt",
}
// Add some flags
cmd.Flags().String("output", "", "Output file")
cmd.Flags().Bool("verbose", false, "Verbose output")
cmd.Flags().IntSlice("include", []int{}, "Include patterns")
cmd.Flags().StringSlice("greeting", []string{"hello", "world"}, "Include patterns")
cmd.Flags().Int("count", 10, "Number of items")
cmd.Flags().StringToString("labels", map[string]string{"hello": "world", "go": "lang"}, "Key-value labels")
cmd.Flags().StringToInt("ports", map[string]int{"life": 42, "power": 9001}, "Port mappings")
// generate schema for a test object
aJSONObjSchema, err := jsonschema.For[SomeJSONObject](nil)
require.NoError(t, err)
bytes, err := aJSONObjSchema.MarshalJSON()
require.NoError(t, err)
// now create flag that has a json schema that represents a json object
cmd.Flags().String("a_json_obj", "", "Some JSON Object")
jsonobj := cmd.Flags().Lookup("a_json_obj")
jsonobj.Annotations = make(map[string][]string)
jsonobj.Annotations["jsonschema"] = []string{string(bytes)}
// generate schema for a test array
aJSONArraySchema, err := jsonschema.For[SomeJSONArray](nil)
require.NoError(t, err)
bytes, err = aJSONArraySchema.MarshalJSON()
require.NoError(t, err)
// now create flag that has a json schema that represents a json array
// note that we can supply a default for the flag here but it's not mapped to the schema default
cmd.Flags().String("a_json_array", "[]", "Some JSON Array")
jsonarray := cmd.Flags().Lookup("a_json_array")
jsonarray.Annotations = make(map[string][]string)
jsonarray.Annotations["jsonschema"] = []string{string(bytes)}
// Add a hidden flag
cmd.Flags().String("hidden", "secret", "Hidden flag")
err = cmd.Flags().MarkHidden("hidden")
require.NoError(t, err)
// Add a deprecated flag
cmd.Flags().String("old", "", "Old flag")
err = cmd.Flags().MarkDeprecated("old", "Use --new instead")
require.NoError(t, err)
// Mark one flag as required
err = cmd.MarkFlagRequired("count")
require.NoError(t, err)
parent := &cobra.Command{
Use: "parent",
Short: "Parent command",
}
// add persistent flag to parent
parent.PersistentFlags().String("config", "", "Config file")
parent.AddCommand(cmd)
t.Run("Default Selector", func(t *testing.T) {
// Create tool from command with a selector that accepts all flags
tool := Selector{}.createToolFromCmd(cmd, "parent")
// Verify tool properties
assert.Equal(t, "parent_test", tool.Name)
assert.Contains(t, tool.Description, "This is a test command")
assert.Contains(t, tool.Description, "test file.txt --output result.txt")
assert.NotNil(t, tool.InputSchema)
// Verify schema structure
inputSchema := tool.InputSchema.(*jsonschema.Schema)
assert.Equal(t, "object", inputSchema.Type)
require.NotNil(t, inputSchema.Properties)
assert.Contains(t, inputSchema.Properties, "flags")
assert.Contains(t, inputSchema.Properties, "args")
// Verify flags schema
flagsSchema := inputSchema.Properties["flags"]
require.NotNil(t, flagsSchema.Properties)
assert.Contains(t, flagsSchema.Properties, "output")
assert.Contains(t, flagsSchema.Properties, "verbose")
assert.Contains(t, flagsSchema.Properties, "include")
assert.Contains(t, flagsSchema.Properties, "count")
assert.Contains(t, flagsSchema.Properties, "greeting")
assert.Contains(t, flagsSchema.Properties, "labels")
assert.Contains(t, flagsSchema.Properties, "ports")
assert.Contains(t, flagsSchema.Properties, "a_json_obj")
assert.Contains(t, flagsSchema.Properties, "a_json_array")
// Verify excluded flags
assert.NotContains(t, flagsSchema.Properties, "hidden", "Should not include hidden flag")
assert.NotContains(t, flagsSchema.Properties, "old", "Should not include deprecated flag")
// Verify flag types
assert.Equal(t, "string", flagsSchema.Properties["output"].Type)
assert.Equal(t, "boolean", flagsSchema.Properties["verbose"].Type)
assert.Equal(t, "array", flagsSchema.Properties["include"].Type)
assert.Equal(t, "integer", flagsSchema.Properties["count"].Type)
assert.Equal(t, "array", flagsSchema.Properties["greeting"].Type)
assert.Equal(t, "object", flagsSchema.Properties["labels"].Type)
assert.Equal(t, "object", flagsSchema.Properties["ports"].Type)
assert.Equal(t, "object", flagsSchema.Properties["a_json_obj"].Type)
assert.True(t, hasSchemaType(flagsSchema.Properties["a_json_array"], "array"), "a_json_array should be an array type")
// Verify required flags
require.Len(t, flagsSchema.Required, 1, "Should have 1 required flag")
assert.Contains(t, flagsSchema.Required, "count", "count flag should be marked as required")
// Verify default values
assert.NotNil(t, flagsSchema.Properties["verbose"].Default)
assert.JSONEq(t, "false", string(flagsSchema.Properties["verbose"].Default))
assert.NotNil(t, flagsSchema.Properties["count"].Default)
assert.JSONEq(t, "10", string(flagsSchema.Properties["count"].Default))
assert.NotNil(t, flagsSchema.Properties["greeting"].Default)
assert.JSONEq(t, `["hello","world"]`, string(flagsSchema.Properties["greeting"].Default))
assert.JSONEq(t, `{"life":42, "power":9001}`, string(flagsSchema.Properties["ports"].Default))
assert.JSONEq(t, `{"hello":"world", "go":"lang"}`, string(flagsSchema.Properties["labels"].Default))
// Empty string and empty array should not have defaults set
assert.Nil(t, flagsSchema.Properties["output"].Default)
assert.Nil(t, flagsSchema.Properties["include"].Default)
// json schema defaults are not populated
assert.Nil(t, flagsSchema.Properties["a_json_obj"].Default)
assert.Nil(t, flagsSchema.Properties["a_json_array"].Default)
// verify json obj schemas - compare key fields rather than full schema
// because PropertyOrder handling changed between jsonschema-go versions
parsedJSONObjSchema := flagsSchema.Properties["a_json_obj"]
assert.Equal(t, aJSONObjSchema.Type, parsedJSONObjSchema.Type)
assert.Equal(t, aJSONObjSchema.Required, parsedJSONObjSchema.Required)
assert.Equal(t, len(aJSONObjSchema.Properties), len(parsedJSONObjSchema.Properties))
// Verify array items schema
includeSchema := flagsSchema.Properties["include"]
assert.NotNil(t, includeSchema.Items)
assert.Equal(t, "integer", includeSchema.Items.Type)
greetingSchema := flagsSchema.Properties["greeting"]
assert.NotNil(t, greetingSchema.Items)
assert.Equal(t, "string", greetingSchema.Items.Type)
// Verify stringToString object schema
labelsSchema := flagsSchema.Properties["labels"]
assert.NotNil(t, labelsSchema.AdditionalProperties)
assert.Equal(t, "string", labelsSchema.AdditionalProperties.Type)
// Verify stringToInt object schema
portsSchema := flagsSchema.Properties["ports"]
assert.NotNil(t, portsSchema.AdditionalProperties)
assert.Equal(t, "integer", portsSchema.AdditionalProperties.Type)
// Verify persistent flag from parent command
assert.Contains(t, flagsSchema.Properties, "config", "Should include persistent flag from parent command")
// Verify args schema
argsSchema := inputSchema.Properties["args"]
assert.True(t, hasSchemaType(argsSchema, "array"), "args should be an array type")
assert.NotNil(t, argsSchema.Items)
assert.Equal(t, "string", argsSchema.Items.Type)
})
t.Run("Restricted Selector", func(t *testing.T) {
// Create a selector that only allows specific flags
selector := Selector{
LocalFlagSelector: func(flag *pflag.Flag) bool {
names := []string{"output", "verbose", "hidden", "old"}
return slices.Contains(names, flag.Name)
},
InheritedFlagSelector: func(_ *pflag.Flag) bool { return false },
}
// Create tool from command with the restricted selector
tool := selector.createToolFromCmd(cmd, "parent")
// Verify tool properties
assert.Equal(t, "parent_test", tool.Name)
assert.Contains(t, tool.Description, "This is a test command")
assert.Contains(t, tool.Description, "test file.txt --output result.txt")
assert.NotNil(t, tool.InputSchema)
// Verify schema structure
inputSchema := tool.InputSchema.(*jsonschema.Schema)
assert.Equal(t, "object", inputSchema.Type)
require.NotNil(t, inputSchema.Properties)
assert.Contains(t, inputSchema.Properties, "flags")
assert.Contains(t, inputSchema.Properties, "args")
// Verify flags schema
flagsSchema := inputSchema.Properties["flags"]
require.NotNil(t, flagsSchema.Properties)
assert.Contains(t, flagsSchema.Properties, "output")
assert.Contains(t, flagsSchema.Properties, "verbose")
// Verify excluded flags
assert.NotContains(t, flagsSchema.Properties, "hidden", "Should not include hidden flag")
assert.NotContains(t, flagsSchema.Properties, "old", "Should not include deprecated flag")
assert.NotContains(t, flagsSchema.Properties, "include", "Should not include excluded flag")
assert.NotContains(t, flagsSchema.Properties, "count", "Should not include excluded flag")
assert.NotContains(t, flagsSchema.Properties, "config", "Should not include excluded persistent flag")
assert.NotContains(t, flagsSchema.Properties, "greeting", "Should not include excluded flag")
assert.NotContains(t, flagsSchema.Properties, "labels", "Should not include excluded flag")
assert.NotContains(t, flagsSchema.Properties, "ports", "Should not include excluded flag")
// Verify required flags - none should be required since 'count' was excluded
require.Empty(t, flagsSchema.Required, "Should have no required flags")
// Verify args schema
argsSchema := inputSchema.Properties["args"]
assert.True(t, hasSchemaType(argsSchema, "array"), "args should be an array type")
assert.NotNil(t, argsSchema.Items)
assert.Equal(t, "string", argsSchema.Items.Type)
})
}
func TestGenerateToolName(t *testing.T) {
root := &cobra.Command{
Use: "root",
}
child := &cobra.Command{
Use: "child",
}
grandchild := &cobra.Command{
Use: "grandchild",
}
root.AddCommand(child)
child.AddCommand(grandchild)
t.Run("Default prefix (uses root name)", func(t *testing.T) {
name := toolName(grandchild, "root")
assert.Equal(t, "root_child_grandchild", name)
})
t.Run("Custom short prefix", func(t *testing.T) {
name := toolName(grandchild, "r")
assert.Equal(t, "r_child_grandchild", name)
})
t.Run("Root command only", func(t *testing.T) {
name := toolName(root, "root")
assert.Equal(t, "root", name)
})
t.Run("Root command with custom prefix", func(t *testing.T) {
name := toolName(root, "myprefix")
assert.Equal(t, "myprefix", name)
})
t.Run("Omnistrate use case - shortening long tool names", func(t *testing.T) {
// Simulates: omnistrate-ctl cost by-instance-type in-provider
omctl := &cobra.Command{Use: "omnistrate-ctl"}
cost := &cobra.Command{Use: "cost"}
byInstanceType := &cobra.Command{Use: "by-instance-type"}
inProvider := &cobra.Command{Use: "in-provider", Run: func(_ *cobra.Command, _ []string) {}}
omctl.AddCommand(cost)
cost.AddCommand(byInstanceType)
byInstanceType.AddCommand(inProvider)
// Using full root name (original behavior)
fullName := toolName(inProvider, "omnistrate-ctl")
assert.Equal(t, "omnistrate-ctl_cost_by-instance-type_in-provider", fullName)
// Using shortened prefix - saves 9 characters (len("omnistrate-ctl") - len("omctl") = 14 - 5 = 9)
shortName := toolName(inProvider, "omctl")
assert.Equal(t, "omctl_cost_by-instance-type_in-provider", shortName)
assert.Less(t, len(shortName), len(fullName), "Short name should be shorter than full name")
assert.Less(t, len(shortName), 64, "Short name should be under Claude's 64-char limit")
})
}
func TestGenerateToolDescription(t *testing.T) {
t.Run("Long and Example", func(t *testing.T) {
cmd1 := &cobra.Command{
Use: "cmd1",
Short: "Short description",
Long: "Long description of cmd1",
Example: "cmd1 --help",
}
desc1 := toolDescription(cmd1)
assert.Contains(t, desc1, "Long description of cmd1")
assert.Contains(t, desc1, "Examples:\ncmd1 --help")
})
t.Run("Short only", func(t *testing.T) {
cmd2 := &cobra.Command{
Use: "cmd2",
Short: "Short description of cmd2",
}
desc2 := toolDescription(cmd2)
assert.Equal(t, "Short description of cmd2", desc2)
})
}