package scenes import ( "bytes" "encoding/json" "fmt" "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v6" ) func TestLoadResponseSchemaForScenes(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v, want nil", err) } if got, want := string(schema.Key), "dnd_scenes_llm"; got != want { t.Fatalf("schema.Key = %q, want %q", got, want) } if got, want := schema.ID, "notarius.dnd.scenes.llm"; got != want { t.Fatalf("schema.ID = %q, want %q", got, want) } if got, want := schema.Version, "v1"; got != want { t.Fatalf("schema.Version = %q, want %q", got, want) } if got, want := schema.Name, "notarius_dnd_scenes_llm_v1"; got != want { t.Fatalf("schema.Name = %q, want %q", got, want) } if !strings.HasPrefix(schema.SHA256, "sha256:") { t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256) } if !json.Valid(schema.JSONSchema) { t.Fatalf("schema.JSONSchema is invalid JSON: %s", schema.JSONSchema) } } func TestResponseSchemaValidatesMinimalSceneResponses(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v, want nil", err) } valid := validSceneSchemaResponse() validJSON, err := json.Marshal(valid) if err != nil { t.Fatalf("Marshal(validSceneSchemaResponse()) error = %v, want nil", err) } if err := validateJSONSchema(validJSON, schema.JSONSchema); err != nil { t.Fatalf("valid scene response rejected: %v", err) } tests := []struct { name string mutate func(map[string]any) }{ { name: "missing scenes", mutate: func(response map[string]any) { delete(response, "scenes") }, }, { name: "empty scenes", mutate: func(response map[string]any) { response["scenes"] = []any{} }, }, { name: "non-positive start unit", mutate: func(response map[string]any) { response["scenes"].([]any)[0].(map[string]any)["start_unit_id"] = 0 }, }, { name: "non-positive end unit", mutate: func(response map[string]any) { response["scenes"].([]any)[0].(map[string]any)["end_unit_id"] = 0 }, }, { name: "non-integer endpoint", mutate: func(response map[string]any) { response["scenes"].([]any)[0].(map[string]any)["start_unit_id"] = 1.5 }, }, { name: "unknown top-level field", mutate: func(response map[string]any) { response["boundary_caveats"] = []any{} }, }, { name: "unknown scene field", mutate: func(response map[string]any) { response["scenes"].([]any)[0].(map[string]any)["short_title"] = "Old contract" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := validSceneSchemaResponse() tt.mutate(response) content, err := json.Marshal(response) if err != nil { t.Fatalf("Marshal() error = %v, want nil", err) } if err := validateJSONSchema(content, schema.JSONSchema); err == nil { t.Fatal("validateJSONSchema() error = nil, want rejected response") } }) } } func TestResponseStructAcceptsIntegerBoundaries(t *testing.T) { raw := []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":3}]}`) var response chunkResponse if err := json.Unmarshal(raw, &response); err != nil { t.Fatalf("Unmarshal() error = %v, want nil", err) } if got := response.Scenes[0].StartUnitID.String(); got != "1" { t.Fatalf("StartUnitID = %q, want 1", got) } if got := response.Scenes[0].EndUnitID.String(); got != "3" { t.Fatalf("EndUnitID = %q, want 3", got) } } func TestResponseSchemaJSONIsMutationSafe(t *testing.T) { first, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v, want nil", err) } first.JSONSchema[0] = '[' second, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v, want nil", err) } if !json.Valid(second.JSONSchema) { t.Fatalf("schema JSON was mutated: %s", second.JSONSchema) } if len(second.JSONSchema) > 0 && second.JSONSchema[0] == '[' { t.Fatal("schema JSON did not use defensive copy") } } func validSceneSchemaResponse() map[string]any { return map[string]any{ "scenes": []any{ map[string]any{ "start_unit_id": 1, "end_unit_id": 3, }, }, } } func validateJSONSchema(instanceContent, schemaContent []byte) error { instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(instanceContent)) if err != nil { return fmt.Errorf("parse instance: %w", err) } schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaContent)) if err != nil { return fmt.Errorf("parse schema: %w", err) } compiler := jsonschema.NewCompiler() if err := compiler.AddResource("schema.json", schemaDocument); err != nil { return fmt.Errorf("load schema: %w", err) } schema, err := compiler.Compile("schema.json") if err != nil { return fmt.Errorf("compile schema: %w", err) } return schema.Validate(instance) }