package scenes import ( "encoding/json" "strings" "testing" ) 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 TestResponseSchemaShapeUsesSourceUnitBoundaries(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v, want nil", err) } var decoded map[string]any if err := json.Unmarshal(schema.JSONSchema, &decoded); err != nil { t.Fatalf("Unmarshal() error = %v, want nil", err) } if decoded["$id"] != ResponseSchemaID { t.Fatalf("$id = %#v, want %q", decoded["$id"], ResponseSchemaID) } if decoded["additionalProperties"] != false { t.Fatalf("additionalProperties = %#v, want false", decoded["additionalProperties"]) } properties := decoded["properties"].(map[string]any) if _, ok := properties["artifact_type"]; ok { t.Fatal("schema includes artifact_type, want only scene response fields") } if _, ok := properties["session_scope"]; ok { t.Fatal("schema includes session_scope, want no session wrapper") } sceneProperties := properties["scenes"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any) for _, field := range []string{"scene_id", "start_segment_id", "end_segment_id"} { if _, ok := sceneProperties[field]; ok { t.Fatalf("scene schema includes old field %q", field) } } for _, field := range []string{"start_unit_id", "end_unit_id"} { property := sceneProperties[field].(map[string]any) if property["type"] != "string" { t.Fatalf("%s type = %#v, want string", field, property["type"]) } } modeEnum := sceneProperties["primary_mode"].(map[string]any)["enum"].([]any) if !sameStrings(modeEnum, []string{"Recap", "Discussion", "Combat", "Narrative"}) { t.Fatalf("primary_mode enum = %#v, want Recap/Discussion/Combat/Narrative", modeEnum) } confidenceEnum := sceneProperties["boundary_confidence"].(map[string]any)["enum"].([]any) if !sameStrings(confidenceEnum, []string{"High", "Medium", "Low"}) { t.Fatalf("boundary_confidence enum = %#v, want High/Medium/Low", confidenceEnum) } } func TestResponseStructRejectsIntegerBoundaries(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 err := json.Unmarshal(raw, &response) if err == nil { t.Fatalf("Unmarshal() error = nil, want integer boundary type error: %#v", response) } if !strings.Contains(err.Error(), "string") { t.Fatalf("Unmarshal() error = %v, want string type error", err) } } 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 sameStrings(got []any, want []string) bool { if len(got) != len(want) { return false } for i := range want { if got[i] != want[i] { return false } } return true }