package plugin import ( "context" "encoding/json" "errors" "strings" "testing" "reasonix/internal/tool" ) func TestCanonicalizeSchemaStable(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}`) first := canonicalizeSchema(schema) second := canonicalizeSchema(first) if string(first) != string(second) { t.Errorf("canonicalizeSchema is not idempotent:\n first: %s\n second: %s", first, second) } } func TestCanonicalizeSchemaSortsRequired(t *testing.T) { schema := json.RawMessage(`{"required":["c","a","b"],"type":"object"}`) result := canonicalizeSchema(schema) var m map[string]any json.Unmarshal(result, &m) arr := m["required"].([]any) if arr[0] != "a" || arr[1] != "b" || arr[2] != "c" { t.Errorf("required not sorted: %v", arr) } } func TestCanonicalizeSchemaPreservesEnum(t *testing.T) { schema := json.RawMessage(`{"enum":["c","a","b"]}`) result := canonicalizeSchema(schema) var m map[string]any json.Unmarshal(result, &m) arr := m["enum"].([]any) if arr[0] != "c" || arr[1] != "a" || arr[2] != "b" { t.Errorf("enum order was changed: %v", arr) } } func TestCanonicalizeSchemaSortsKeys(t *testing.T) { schema := json.RawMessage(`{"z":1,"a":2,"m":3}`) result := canonicalizeSchema(schema) // json.Marshal sorts map keys, so verify the JSON string directly. s := string(result) if s != `{"a":2,"m":3,"z":1}` { t.Errorf("keys not sorted, got: %s", s) } } func TestCanonicalizeSchemaNested(t *testing.T) { schema := json.RawMessage(`{"properties":{"inner":{"type":"object","required":["b","a"]}}}`) result := canonicalizeSchema(schema) var m map[string]any json.Unmarshal(result, &m) props := m["properties"].(map[string]any) inner := props["inner"].(map[string]any) req := inner["required"].([]any) if req[0] != "a" || req[1] != "b" { t.Errorf("nested required not sorted: %v", req) } } func TestCanonicalizeSchemaEquivalentOrderingMatches(t *testing.T) { first := canonicalizeSchema(json.RawMessage(`{"type":"object","required":["b","a"],"properties":{"b":{"description":"bee","type":"string"},"a":{"type":"integer"}}}`)) second := canonicalizeSchema(json.RawMessage(`{"properties":{"a":{"type":"integer"},"b":{"type":"string","description":"bee"}},"required":["a","b"],"type":"object"}`)) if string(first) != string(second) { t.Fatalf("equivalent schemas canonicalized differently:\n first: %s\n second: %s", first, second) } } func TestRemoteToolSchemaCanonicalizesOnReturn(t *testing.T) { rt := &remoteTool{schema: json.RawMessage(`{"type":"object","required":["z","a"],"properties":{"z":{"type":"string"},"a":{"type":"string"}}}`)} if got, want := string(rt.Schema()), `{"properties":{"a":{"type":"string"},"z":{"type":"string"}},"required":["a","z"],"type":"object"}`; got != want { t.Fatalf("Schema() = %s, want %s", got, want) } } func TestSortToolsByName(t *testing.T) { tools := []tool.Tool{ testTool{name: "zulu"}, testTool{name: "alpha"}, testTool{name: "mike"}, } sorted := sortToolsByName(tools) if sorted[0].Name() != "alpha" || sorted[1].Name() != "mike" || sorted[2].Name() != "zulu" { t.Errorf("tools not sorted: %v", toolNames(sorted)) } // Original should be unchanged if tools[0].Name() != "zulu" { t.Error("original slice was mutated") } } func TestNormalizeNameForToolNames(t *testing.T) { if got := normalizeName("valid_name-1"); got != "valid_name-1" { t.Fatalf("valid name changed: %q", got) } cases := []string{"@modelcontextprotocol/server-memory", "mcp server/fetch", " "} for _, in := range cases { got := normalizeName(in) if got == "" || strings.ContainsAny(got, " @/") { t.Errorf("normalizeName(%q) = %q, want non-empty safe identifier", in, got) } } } func TestNormalizeNameAvoidsSanitizedCollisions(t *testing.T) { a := normalizeName("search/code") b := normalizeName("search_code") if a == b { t.Fatalf("normalized names collided: %q", a) } if b != "search_code" { t.Fatalf("valid identifier should stay stable, got %q", b) } if normalizeName("@foo") == normalizeName("foo") { t.Fatal("trimmed invalid prefix should not collapse onto valid name") } } func TestSummarizeFailureErrorSingleLine(t *testing.T) { got := summarizeFailureError(errors.New("npm error code ENOTEMPTY\nnpm error path /tmp/x")) if strings.Contains(got, "\n") || !strings.Contains(got, "ENOTEMPTY") { t.Fatalf("summary = %q", got) } } type testTool struct{ name string } func (t testTool) Name() string { return t.name } func (t testTool) Description() string { return "" } func (t testTool) Schema() json.RawMessage { return nil } func (t testTool) Execute(ctx context.Context, args json.RawMessage) (string, error) { return "", nil } func (t testTool) ReadOnly() bool { return true } func toolNames(ts []tool.Tool) []string { names := make([]string, len(ts)) for i, t := range ts { names[i] = t.Name() } return names }