返回 DeepSeek-Reasonix
arguments_test.go
根目录 / internal / tool / arguments_test.go
1 package tool
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 )
11
12 type schemaTool struct {
13 name string
14 schema json.RawMessage
15 }
16
17 func (s schemaTool) Name() string { return s.name }
18 func (s schemaTool) Description() string { return "" }
19 func (s schemaTool) Schema() json.RawMessage { return s.schema }
20 func (schemaTool) ReadOnly() bool { return true }
21 func (schemaTool) Execute(context.Context, json.RawMessage) (string, error) { return "", nil }
22
23 type stubMCPTool struct{ schemaTool }
24
25 func (stubMCPTool) MCPServerName() string { return "srv" }
26 func (stubMCPTool) MCPRawToolName() string { return "tool" }
27
28 func TestValidateArgumentsDraftsAndEnums(t *testing.T) {
29 schema2020 := json.RawMessage(`{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"mode":{"type":"string","enum":["a","b"]}},"required":["mode"],"additionalProperties":false}`)
30 target := schemaTool{name: "t", schema: schema2020}
31 if got := ValidateArguments(target, json.RawMessage(`{"mode":"a"}`)); len(got.Violations) != 0 || got.CompileErr != nil {
32 t.Fatalf("valid args: %+v", got)
33 }
34 got := ValidateArguments(target, json.RawMessage(`{"mode":"z"}`))
35 if len(got.Violations) == 0 {
36 t.Fatal("enum mismatch accepted")
37 }
38 if got.Violations[0].Keyword != "enum" || strings.Contains(got.Violations[0].Expected, "z") {
39 t.Fatalf("violation leaked value or missed enum: %+v", got.Violations[0])
40 }
41 }
42
43 func TestValidateArgumentsDefaultDraft2020AndDraft7Fallback(t *testing.T) {
44 explicit := json.RawMessage(`{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"n":{"type":"integer","minimum":1}},"required":["n"]}`)
45 got := ValidateArguments(schemaTool{name: "d7", schema: explicit}, json.RawMessage(`{"n":2}`))
46 if got.CompileErr != nil || len(got.Violations) != 0 {
47 t.Fatalf("explicit draft-07: %+v", got)
48 }
49 implicit := json.RawMessage(`{"type":"object","properties":{"n":{"type":"integer"}}}`)
50 got = ValidateArguments(schemaTool{name: "d2020", schema: implicit}, json.RawMessage(`{"n":2}`))
51 if got.CompileErr != nil {
52 t.Fatalf("implicit 2020-12 compile: %v", got.CompileErr)
53 }
54 }
55
56 func TestValidateArgumentsRejectsExternalRefs(t *testing.T) {
57 path := filepath.Join(t.TempDir(), "args.json")
58 if err := os.WriteFile(path, []byte(`{"type":"string"}`), 0o600); err != nil {
59 t.Fatal(err)
60 }
61 fileURL := "file:///" + strings.TrimPrefix(filepath.ToSlash(path), "/")
62 raw := json.RawMessage(`{"type":"object","properties":{"x":{"$ref":"` + fileURL + `"}}}`)
63 got := ValidateArguments(schemaTool{name: "builtin", schema: raw}, json.RawMessage(`{"x":"a"}`))
64 if got.CompileErr == nil {
65 t.Fatal("built-in schema with file $ref must be a compile error")
66 }
67 skipped := ValidateArguments(stubMCPTool{schemaTool{name: "mcp", schema: raw}}, json.RawMessage(`{"x":"a"}`))
68 if !skipped.Skipped {
69 t.Fatalf("third-party MCP uncompilable schema must skip, got %+v", skipped)
70 }
71 }
72
73 func TestInvalidateArgumentSchemasDropsCompiledValidator(t *testing.T) {
74 schema := json.RawMessage(`{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}`)
75 target := schemaTool{name: "t", schema: schema}
76 if got := ValidateArguments(target, json.RawMessage(`{"n":1}`)); got.CompileErr != nil {
77 t.Fatalf("compile: %v", got.CompileErr)
78 }
79 fp := SchemaFingerprint(schema)
80 if _, ok := argumentSchemaCache.Load(fp); !ok {
81 t.Fatal("expected compiled validator cache entry")
82 }
83 InvalidateArgumentSchemas([]string{fp})
84 if _, ok := argumentSchemaCache.Load(fp); ok {
85 t.Fatal("invalidated fingerprint still cached")
86 }
87 }
88
89 func TestValidateArgumentsNullBecomesObject(t *testing.T) {
90 target := schemaTool{name: "t", schema: json.RawMessage(`{"type":"object"}`)}
91 if got := ValidateArguments(target, nil); len(got.Violations) != 0 {
92 t.Fatalf("nil args: %+v", got)
93 }
94 if got := ValidateArguments(target, json.RawMessage(`null`)); len(got.Violations) != 0 {
95 t.Fatalf("null args: %+v", got)
96 }
97 }
98
99 func TestValidateArgumentsCapsViolations(t *testing.T) {
100 props := map[string]any{}
101 required := make([]string, 0, 12)
102 for i := range 12 {
103 name := "f" + string(rune('a'+i))
104 props[name] = map[string]any{"type": "string"}
105 required = append(required, name)
106 }
107 schema, _ := json.Marshal(map[string]any{"type": "object", "properties": props, "required": required})
108 got := ValidateArguments(schemaTool{name: "t", schema: schema}, json.RawMessage(`{}`))
109 if len(got.Violations) > maxArgumentViolations {
110 t.Fatalf("violations = %d, want <= %d", len(got.Violations), maxArgumentViolations)
111 }
112 }
113
114 func TestSchemaFingerprintStable(t *testing.T) {
115 raw := json.RawMessage(`{"type":"object"}`)
116 copyRaw := append(json.RawMessage(nil), raw...)
117 if SchemaFingerprint(raw) != SchemaFingerprint(copyRaw) || SchemaFingerprint(raw) == "" {
118 t.Fatal("fingerprint not stable")
119 }
120 }
121
121 lines GO