package semanticreconcile import ( "bytes" "encoding/json" "reflect" "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v6" ) func TestResponseSchemaMetadataAndOwnership(t *testing.T) { schema, err := LoadResponseSchema() if err != nil { t.Fatal(err) } if schema.Key != ResponseSchemaKey || schema.ID != ResponseSchemaID || schema.Version != SchemaVersion || schema.Name != ResponseSchemaName { t.Fatalf("schema metadata = %#v", schema) } if !json.Valid(schema.JSONSchema) || !strings.HasPrefix(schema.SHA256, "sha256:") { t.Fatalf("schema content metadata = %#v", schema) } first := append([]byte(nil), schema.JSONSchema...) schema.JSONSchema[0] = '[' loadedAgain, err := LoadResponseSchema() if err != nil { t.Fatal(err) } if !bytes.Equal(loadedAgain.JSONSchema, first) || !json.Valid(loadedAgain.JSONSchema) { t.Fatal("LoadResponseSchema() exposed shared schema content") } } func TestResponseSchemaAcceptsOnlyTheIntegerProposalShape(t *testing.T) { schema, err := LoadResponseSchema() if err != nil { t.Fatal(err) } tests := []struct { name string value any valid bool }{ {name: "empty proposal", value: map[string]any{"duplicate_groups": []any{}}, valid: true}, {name: "valid group", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}, "canonical_candidate_id": 1}}}, valid: true}, {name: "semantic canonical mismatch", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}, "canonical_candidate_id": 3}}}, valid: true}, {name: "missing proposal", value: map[string]any{}, valid: false}, {name: "unknown top-level field", value: map[string]any{"duplicate_groups": []any{}, "extra": true}, valid: false}, {name: "missing members", value: map[string]any{"duplicate_groups": []any{map[string]any{"canonical_candidate_id": 1}}}, valid: false}, {name: "missing canonical", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}}}}, valid: false}, {name: "unknown group field", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}, "canonical_candidate_id": 1, "name": "replacement"}}}, valid: false}, {name: "too few members", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1}, "canonical_candidate_id": 1}}}, valid: false}, {name: "repeated members", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 1}, "canonical_candidate_id": 1}}}, valid: false}, {name: "zero member", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{0, 1}, "canonical_candidate_id": 1}}}, valid: false}, {name: "negative member", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{-1, 1}, "canonical_candidate_id": 1}}}, valid: false}, {name: "non-integer member", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2.5}, "canonical_candidate_id": 1}}}, valid: false}, {name: "zero canonical", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}, "canonical_candidate_id": 0}}}, valid: false}, {name: "contextual selectors", value: map[string]any{"duplicate_groups": []any{map[string]any{"candidate_ids": []any{1, 2}, "canonical_candidate_id": 1, "source_refs": []any{}}}}, valid: false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { content, err := json.Marshal(test.value) if err != nil { t.Fatal(err) } err = validateAgainstSchema(content, schema.JSONSchema) if (err == nil) != test.valid { t.Fatalf("schema validation error = %v, want valid=%t", err, test.valid) } }) } content := []byte(`{"duplicate_groups":[{"candidate_ids":[1,2],"canonical_candidate_id":2}]}`) if err := validateAgainstSchema(content, schema.JSONSchema); err != nil { t.Fatal(err) } var response ProposalResponse if err := json.Unmarshal(content, &response); err != nil { t.Fatal(err) } want := ProposalResponse{DuplicateGroups: []DuplicateGroup{{CandidateIDs: []int{1, 2}, CanonicalCandidateID: 2}}} if !reflect.DeepEqual(response, want) { t.Fatalf("decoded response = %#v, want %#v", response, want) } } func validateAgainstSchema(instanceContent, schemaContent []byte) error { instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(instanceContent)) if err != nil { return err } document, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaContent)) if err != nil { return err } compiler := jsonschema.NewCompiler() if err := compiler.AddResource("schema.json", document); err != nil { return err } compiled, err := compiler.Compile("schema.json") if err != nil { return err } return compiled.Validate(instance) }