| 1 | package browser |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "slices" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/provider" |
| 10 | ) |
| 11 | |
| 12 | func TestSchemasAreCanonicalAndClosed(t *testing.T) { |
| 13 | first := Tools(&fakeExecutor{}) |
| 14 | second := Tools(&fakeExecutor{}) |
| 15 | for i, tl := range first { |
| 16 | name := tl.Name() |
| 17 | raw := tl.Schema() |
| 18 | if !json.Valid(raw) { |
| 19 | t.Errorf("%s schema is invalid JSON: %s", name, raw) |
| 20 | continue |
| 21 | } |
| 22 | if got := string(provider.CanonicalizeSchema(raw)); got != string(raw) { |
| 23 | t.Errorf("%s schema is not canonical:\n got %s\nwant %s", name, raw, got) |
| 24 | } |
| 25 | if string(tl.Schema()) != string(raw) || string(second[i].Schema()) != string(raw) { |
| 26 | t.Errorf("%s schema is not deterministic", name) |
| 27 | } |
| 28 | var s struct { |
| 29 | Type string `json:"type"` |
| 30 | AdditionalProperties *bool `json:"additionalProperties"` |
| 31 | Properties map[string]json.RawMessage `json:"properties"` |
| 32 | Required *[]string `json:"required"` |
| 33 | } |
| 34 | if err := json.Unmarshal(raw, &s); err != nil { |
| 35 | t.Fatalf("%s: %v", name, err) |
| 36 | } |
| 37 | if s.Type != "object" || s.AdditionalProperties == nil || *s.AdditionalProperties { |
| 38 | t.Errorf("%s schema is not a closed object: %s", name, raw) |
| 39 | } |
| 40 | if s.Required == nil { |
| 41 | t.Errorf("%s schema declares no required list", name) |
| 42 | continue |
| 43 | } |
| 44 | if len(*s.Required) == 0 && name != "browser_tabs" { |
| 45 | t.Errorf("%s schema has an empty required list", name) |
| 46 | } |
| 47 | if !slices.IsSorted(*s.Required) { |
| 48 | t.Errorf("%s required is not sorted: %v", name, *s.Required) |
| 49 | } |
| 50 | for _, r := range *s.Required { |
| 51 | if _, ok := s.Properties[r]; !ok { |
| 52 | t.Errorf("%s requires undeclared property %q", name, r) |
| 53 | } |
| 54 | } |
| 55 | if readOnlyTools[name] { |
| 56 | if _, ok := s.Properties["operationId"]; ok { |
| 57 | t.Errorf("%s is read-only but declares operationId", name) |
| 58 | } |
| 59 | continue |
| 60 | } |
| 61 | if !slices.Contains(*s.Required, "operationId") { |
| 62 | t.Errorf("%s is a write but does not require operationId: %v", name, *s.Required) |
| 63 | } |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestSchemaTeachesWorkflow(t *testing.T) { |
| 68 | click := string(toolByName(t, nil, "browser_click").Schema()) |
| 69 | if !strings.Contains(click, `"pattern":"^[A-Za-z0-9_-]{1,100}$"`) { |
| 70 | t.Errorf("browser_click operationId lacks the pattern: %s", click) |
| 71 | } |
| 72 | for _, want := range []string{"never reuse", "documentToken", "browser_snapshot"} { |
| 73 | if !strings.Contains(click, want) { |
| 74 | t.Errorf("browser_click schema does not mention %q", want) |
| 75 | } |
| 76 | } |
| 77 | nav := string(toolByName(t, nil, "browser_navigate").Schema()) |
| 78 | if !strings.Contains(nav, `"enum":["url","back","forward","reload"]`) { |
| 79 | t.Errorf("browser_navigate action enum missing: %s", nav) |
| 80 | } |
| 81 | if got := string(toolByName(t, nil, "browser_tabs").Schema()); got != `{"additionalProperties":false,"properties":{},"required":[],"type":"object"}` { |
| 82 | t.Errorf("browser_tabs schema = %s", got) |
| 83 | } |
| 84 | } |
| 85 |