Files
notarius/internal/modules/dnd/chunk/scenes/schema_test.go

203 lines
5.5 KiB
Go

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 schema.Key != ResponseSchemaKey {
t.Fatalf("schema.Key = %q, want %q", schema.Key, ResponseSchemaKey)
}
if schema.ID != ResponseSchemaID {
t.Fatalf("schema.ID = %q, want %q", schema.ID, ResponseSchemaID)
}
if schema.Version != ResponseSchemaVersion {
t.Fatalf("schema.Version = %q, want %q", schema.Version, ResponseSchemaVersion)
}
if schema.Name != ResponseSchemaName {
t.Fatalf("schema.Name = %q, want %q", schema.Name, ResponseSchemaName)
}
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 TestResponseSchemaValidatesSceneResponses(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: "obsolete segment boundaries",
mutate: func(response map[string]any) {
scene := response["scenes"].([]any)[0].(map[string]any)
scene["start_segment_id"] = 1
scene["end_segment_id"] = 2
},
},
{
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: "invalid primary mode",
mutate: func(response map[string]any) {
response["scenes"].([]any)[0].(map[string]any)["primary_mode"] = "Unknown"
},
},
{
name: "invalid boundary confidence",
mutate: func(response map[string]any) {
response["scenes"].([]any)[0].(map[string]any)["boundary_confidence"] = "Unknown"
},
},
{
name: "empty boundary caveat",
mutate: func(response map[string]any) {
response["boundary_caveats"] = []any{""}
},
},
{
name: "unknown property",
mutate: func(response map[string]any) {
response["unexpected"] = true
},
},
}
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,
"short_title": "Ambush",
"primary_mode": "Combat",
"main_participants": ["Aria"],
"summary": "The party fights.",
"boundary_note": "Combat starts and resolves.",
"boundary_confidence": "High"
}
],
"boundary_caveats": []
}`)
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.Fatalf("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,
"short_title": "Ambush",
"primary_mode": "Combat",
"main_participants": []any{"Aria"},
"summary": "The party fights.",
"boundary_note": "Combat starts and resolves.",
"boundary_confidence": "High",
},
},
"boundary_caveats": []any{},
}
}
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)
}