Test checks that the JSON schema for a tool has not changed unexpectedly. It compares the marshaled JSON of the provided tool against a stored snapshot file. If the UPDATE_TOOLSNAPS environment variable is set to "true", it updates the snapshot file instead. If the snapshot does not exist and not ru
(toolName string, tool any)
| 19 | // If the snapshot exists, it compares the tool's JSON to the snapshot and returns an error if they differ. |
| 20 | // Returns an error if marshaling, reading, or comparing fails. |
| 21 | func Test(toolName string, tool any) error { |
| 22 | toolJSON, err := json.MarshalIndent(tool, "", " ") |
| 23 | if err != nil { |
| 24 | return fmt.Errorf("failed to marshal tool %s: %w", toolName, err) |
| 25 | } |
| 26 | |
| 27 | snapPath := fmt.Sprintf("__toolsnaps__/%s.snap", toolName) |
| 28 | |
| 29 | // If UPDATE_TOOLSNAPS is set, then we write the tool JSON to the snapshot file and exit |
| 30 | if os.Getenv("UPDATE_TOOLSNAPS") == "true" { |
| 31 | return writeSnap(snapPath, toolJSON) |
| 32 | } |
| 33 | |
| 34 | snapJSON, err := os.ReadFile(snapPath) //nolint:gosec // filepaths are controlled by the test suite, so this is safe. |
| 35 | // If the snapshot file does not exist, this must be the first time this test is run. |
| 36 | // We write the tool JSON to the snapshot file and exit. |
| 37 | if os.IsNotExist(err) { |
| 38 | // If we're running in CI, we will error if there is not snapshot because it's important that snapshots |
| 39 | // are committed alongside the tests, rather than just being constructed and not committed during a CI run. |
| 40 | if os.Getenv("GITHUB_ACTIONS") == "true" { |
| 41 | return fmt.Errorf("tool snapshot does not exist for %s. Please run the tests with UPDATE_TOOLSNAPS=true to create it", toolName) |
| 42 | } |
| 43 | |
| 44 | return writeSnap(snapPath, toolJSON) |
| 45 | } |
| 46 | |
| 47 | // Otherwise we will compare the tool JSON to the snapshot JSON |
| 48 | toolNode, err := jd.ReadJsonString(string(toolJSON)) |
| 49 | if err != nil { |
| 50 | return fmt.Errorf("failed to parse tool JSON for %s: %w", toolName, err) |
| 51 | } |
| 52 | |
| 53 | snapNode, err := jd.ReadJsonString(string(snapJSON)) |
| 54 | if err != nil { |
| 55 | return fmt.Errorf("failed to parse snapshot JSON for %s: %w", toolName, err) |
| 56 | } |
| 57 | |
| 58 | // jd.Set allows arrays to be compared without order sensitivity, |
| 59 | // which is useful because we don't really care about this when exposing tool schemas. |
| 60 | diff := toolNode.Diff(snapNode, jd.SET).Render() |
| 61 | if diff != "" { |
| 62 | // If there is a difference, we return an error with the diff |
| 63 | return fmt.Errorf("tool schema for %s has changed unexpectedly:\n%s\nrun with `UPDATE_TOOLSNAPS=true` if this is expected", toolName, diff) |
| 64 | } |
| 65 | |
| 66 | return nil |
| 67 | } |
| 68 | |
| 69 | func writeSnap(snapPath string, contents []byte) error { |
| 70 | // Sort the JSON keys recursively to ensure consistent output. |
no test coverage detected