sanitize_test.go 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. package mcp
  2. import (
  3. "encoding/json"
  4. "math"
  5. "testing"
  6. )
  7. // TestSanitizeNonFiniteFloatsAssetResponseMarshals reproduces the integration
  8. // failure (TestMCPAssetVsHTTP: "marshaling output: json: unsupported value:
  9. // NaN") and verifies the sanitizer fixes it: encoding/json must reject the
  10. // response before sanitization and accept it after, with non-finite floats
  11. // zeroed and finite ones preserved.
  12. func TestSanitizeNonFiniteFloatsAssetResponseMarshals(t *testing.T) {
  13. usedBytes := math.NaN()
  14. resp := &AssetResponse{
  15. Assets: map[string]*AssetSet{
  16. "assets": &AssetSet{
  17. Name: "assets",
  18. Assets: []*Asset{
  19. &Asset{
  20. Type: "Node",
  21. Minutes: math.NaN(),
  22. Adjustment: math.Inf(1),
  23. TotalCost: math.Inf(-1),
  24. CPUCost: math.NaN(),
  25. GPUCost: 5.0, // finite, must be preserved
  26. ByteHoursUsed: &usedBytes,
  27. Overhead: &NodeOverhead{OverheadCostFraction: math.NaN()},
  28. CPUBreakdown: &AssetBreakdown{Idle: math.NaN()},
  29. },
  30. },
  31. },
  32. },
  33. }
  34. if _, err := json.Marshal(resp); err == nil {
  35. t.Fatal("expected json.Marshal to fail before sanitization (NaN/Inf present)")
  36. }
  37. resp = sanitizeNonFiniteFloats(resp).(*AssetResponse)
  38. if _, err := json.Marshal(resp); err != nil {
  39. t.Fatalf("expected json.Marshal to succeed after sanitization, got %v", err)
  40. }
  41. a := resp.Assets["assets"].Assets[0]
  42. if a.Minutes != 0 || a.Adjustment != 0 || a.TotalCost != 0 || a.CPUCost != 0 {
  43. t.Fatalf("expected non-finite base floats zeroed, got %+v", a)
  44. }
  45. if a.GPUCost != 5.0 {
  46. t.Fatalf("expected finite GPUCost preserved, got %v", a.GPUCost)
  47. }
  48. if a.ByteHoursUsed == nil || *a.ByteHoursUsed != 0 {
  49. t.Fatalf("expected non-finite *float64 zeroed, got %v", a.ByteHoursUsed)
  50. }
  51. if a.Overhead.OverheadCostFraction != 0 {
  52. t.Fatalf("expected nested overhead fraction zeroed, got %v", a.Overhead.OverheadCostFraction)
  53. }
  54. if a.CPUBreakdown.Idle != 0 {
  55. t.Fatalf("expected nested breakdown value zeroed, got %v", a.CPUBreakdown.Idle)
  56. }
  57. }
  58. // TestSanitizeNonFiniteFloatsValueType verifies a non-pointer (value) input is
  59. // sanitized via the returned copy, not just pointers.
  60. func TestSanitizeNonFiniteFloatsValueType(t *testing.T) {
  61. in := Asset{TotalCost: math.NaN(), GPUCost: 3.0}
  62. out, ok := sanitizeNonFiniteFloats(in).(Asset)
  63. if !ok {
  64. t.Fatalf("expected Asset back, got %T", sanitizeNonFiniteFloats(in))
  65. }
  66. if out.TotalCost != 0 {
  67. t.Fatalf("expected NaN zeroed in returned value, got %v", out.TotalCost)
  68. }
  69. if out.GPUCost != 3.0 {
  70. t.Fatalf("expected finite value preserved, got %v", out.GPUCost)
  71. }
  72. }
  73. func TestSanitizeNonFiniteFloatsNilSafe(t *testing.T) {
  74. if got := sanitizeNonFiniteFloats(nil); got != nil {
  75. t.Fatalf("expected nil, got %v", got)
  76. }
  77. var p *AssetResponse
  78. sanitizeNonFiniteFloats(p)
  79. sanitizeNonFiniteFloats(&AssetResponse{})
  80. }