package combatturns import ( "bytes" "encoding/json" "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v6" ) func TestLoadResponseSchemaUsesPrivateCombatShape(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatalf("loadResponseSchema() error = %v", err) } if schema.Key != ResponseSchemaKey || schema.ID != ResponseSchemaID || schema.Version != SchemaVersion || schema.Name != ResponseSchemaName || !strings.HasPrefix(schema.SHA256, "sha256:") || !json.Valid(schema.JSONSchema) { t.Fatalf("schema metadata = %#v, want private combat schema identity", schema) } valid := validCombatResponse() content, err := json.Marshal(valid) if err != nil { t.Fatal(err) } if err := validateJSONSchema(content, schema.JSONSchema); err != nil { t.Fatalf("valid combat response rejected: %v", err) } withSourceID := validCombatResponse() withSourceID["combat_turns"].([]any)[0].(map[string]any)["source_refs"].([]any)[0].(map[string]any)["source_id"] = "session-alpha" content, err = json.Marshal(withSourceID) if err != nil { t.Fatal(err) } if err := validateJSONSchema(content, schema.JSONSchema); err == nil { t.Fatal("response schema accepted source_id, want private source-reference shape") } } func TestResponseSchemaLeavesSemanticConstraintsToDeterministicValidators(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatal(err) } semanticCandidate := validCombatResponse() turn := semanticCandidate["combat_turns"].([]any)[0].(map[string]any) turn["actor"] = "" turn["turn_kind"] = "unsupported" turn["round"] = -1 turn["summary"] = "" action := turn["actions"].([]any)[0].(map[string]any) action["category"] = "unsupported" action["declaration"] = "" action["targets"] = []any{""} action["resolution"] = "" ref := turn["source_refs"].([]any)[0].(map[string]any) ref["start_unit_id"] = 0 ref["end_unit_id"] = -1 content, err := json.Marshal(semanticCandidate) if err != nil { t.Fatal(err) } if err := validateJSONSchema(content, schema.JSONSchema); err != nil { t.Fatalf("private schema rejected validator-owned semantics: %v", err) } turn["actions"] = []any{} turn["source_refs"] = []any{} content, err = json.Marshal(semanticCandidate) if err != nil { t.Fatal(err) } if err := validateJSONSchema(content, schema.JSONSchema); err != nil { t.Fatalf("private schema rejected empty validator-owned collections: %v", err) } } func TestResponseSchemaRetainsStructuralBoundary(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatal(err) } for _, test := range []struct { name string mutate func(map[string]any) }{ {name: "missing nullable round", mutate: func(turn map[string]any) { delete(turn, "round") }}, {name: "wrong round type", mutate: func(turn map[string]any) { turn["round"] = "one" }}, {name: "unknown field", mutate: func(turn map[string]any) { turn["unexpected"] = true }}, {name: "missing nullable resolution", mutate: func(turn map[string]any) { delete(turn["actions"].([]any)[0].(map[string]any), "resolution") }}, } { t.Run(test.name, func(t *testing.T) { candidate := validCombatResponse() test.mutate(candidate["combat_turns"].([]any)[0].(map[string]any)) content, err := json.Marshal(candidate) if err != nil { t.Fatal(err) } if err := validateJSONSchema(content, schema.JSONSchema); err == nil { t.Fatal("private schema accepted structurally invalid response") } }) } } func TestResponseSchemaJSONIsMutationSafe(t *testing.T) { first, err := loadResponseSchema() if err != nil { t.Fatal(err) } first.JSONSchema[0] = '[' second, err := loadResponseSchema() if err != nil || !json.Valid(second.JSONSchema) || bytes.Equal(first.JSONSchema, second.JSONSchema) { t.Fatalf("second schema = %s, %v; want defensive valid copy", second.JSONSchema, err) } } func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) { schema, err := loadResponseSchema() if err != nil { t.Fatal(err) } diagnostics := schema.DiagnosticsMap() if diagnostics["key"] != ResponseSchemaKey || diagnostics["id"] != ResponseSchemaID { t.Fatalf("diagnostics = %#v, want schema identity", diagnostics) } if _, ok := diagnostics["json_schema"]; ok { t.Fatalf("diagnostics include raw schema: %#v", diagnostics) } } func validCombatResponse() map[string]any { return map[string]any{ "combat_turns": []any{ map[string]any{ "actor": "Aria", "turn_kind": "reaction", "round": nil, "actions": []any{map[string]any{ "category": "attack", "declaration": "Aria strikes", "targets": []any{}, "resolution": nil, }}, "summary": "Aria reacts.", "source_refs": []any{map[string]any{"start_unit_id": 1, "end_unit_id": 2}}, }, }, } } func validateJSONSchema(instanceContent, schemaContent []byte) error { instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(instanceContent)) if err != nil { return err } schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaContent)) if err != nil { return err } compiler := jsonschema.NewCompiler() if err := compiler.AddResource("schema.json", schemaDocument); err != nil { return err } schema, err := compiler.Compile("schema.json") if err != nil { return err } return schema.Validate(instance) }