| 1 | package boot |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "reflect" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/provider" |
| 10 | ) |
| 11 | |
| 12 | func TestBootToolContractMatchesProviderVisibleSurface(t *testing.T) { |
| 13 | for _, tc := range []struct { |
| 14 | name string |
| 15 | tokenMode string |
| 16 | }{ |
| 17 | {name: "default", tokenMode: ""}, |
| 18 | {name: "economy", tokenMode: "economy"}, |
| 19 | } { |
| 20 | t.Run(tc.name, func(t *testing.T) { |
| 21 | isolateConfigHome(t) |
| 22 | dir := robustTempDir(t) |
| 23 | t.Chdir(dir) |
| 24 | writeFile(t, dir, "reasonix.toml", ` |
| 25 | default_model = "test-model" |
| 26 | |
| 27 | [agent] |
| 28 | system_prompt = "BASE" |
| 29 | |
| 30 | [[providers]] |
| 31 | name = "test-model" |
| 32 | kind = "boot-token-profile-test" |
| 33 | model = "x" |
| 34 | `) |
| 35 | |
| 36 | req, entries := captureTokenProfileSurface(t, tc.tokenMode) |
| 37 | wantNames := unifiedBootToolNames() |
| 38 | if got := toolSchemaNames(req.Tools); !reflect.DeepEqual(got, wantNames) { |
| 39 | t.Fatalf("%s provider-visible tool surface changed\ngot %v\nwant %v", tc.name, got, wantNames) |
| 40 | } |
| 41 | if len(entries) != len(req.Tools) { |
| 42 | t.Fatalf("contract entries = %d, provider tools = %d\ncontract=%v\nprovider=%v", len(entries), len(req.Tools), contractEntryNames(entries), toolSchemaNames(req.Tools)) |
| 43 | } |
| 44 | for i, e := range entries { |
| 45 | s := req.Tools[i] |
| 46 | if e.Name != s.Name { |
| 47 | t.Fatalf("tool[%d] name = %q, want %q\ncontract=%v\nprovider=%v", i, e.Name, s.Name, contractEntryNames(entries), toolSchemaNames(req.Tools)) |
| 48 | } |
| 49 | if e.Description != strings.TrimSpace(s.Description) { |
| 50 | t.Fatalf("%s description drift\ncontract=%q\nprovider=%q", e.Name, e.Description, s.Description) |
| 51 | } |
| 52 | if !json.Valid(e.Schema) { |
| 53 | t.Fatalf("%s contract schema is invalid JSON: %s", e.Name, e.Schema) |
| 54 | } |
| 55 | if got := string(provider.CanonicalizeSchema(e.Schema)); got != string(e.Schema) { |
| 56 | t.Fatalf("%s contract schema is not canonical", e.Name) |
| 57 | } |
| 58 | if string(e.Schema) != string(s.Parameters) { |
| 59 | t.Fatalf("%s schema drift\ncontract=%s\nprovider=%s", e.Name, e.Schema, s.Parameters) |
| 60 | } |
| 61 | } |
| 62 | readOnly := map[string]bool{} |
| 63 | for _, e := range entries { |
| 64 | readOnly[e.Name] = e.ReadOnly |
| 65 | } |
| 66 | for name, want := range map[string]bool{ |
| 67 | platformShellToolName(): false, |
| 68 | "read_file": true, |
| 69 | "view_image": true, |
| 70 | "use_capability": true, |
| 71 | } { |
| 72 | got, ok := readOnly[name] |
| 73 | if !ok { |
| 74 | t.Fatalf("contract missing %s; tools=%v", name, contractEntryNames(entries)) |
| 75 | } |
| 76 | if got != want { |
| 77 | t.Fatalf("%s ReadOnly = %v, want %v", name, got, want) |
| 78 | } |
| 79 | } |
| 80 | if _, ok := readOnly["connect_tool_source"]; ok { |
| 81 | t.Fatalf("connect_tool_source must not appear on the provider-visible surface") |
| 82 | } |
| 83 | }) |
| 84 | } |
| 85 | } |
| 86 |