|
|
@@ -0,0 +1,88 @@
|
|
|
+package mcp
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "math"
|
|
|
+ "testing"
|
|
|
+)
|
|
|
+
|
|
|
+// TestSanitizeNonFiniteFloatsAssetResponseMarshals reproduces the integration
|
|
|
+// failure (TestMCPAssetVsHTTP: "marshaling output: json: unsupported value:
|
|
|
+// NaN") and verifies the sanitizer fixes it: encoding/json must reject the
|
|
|
+// response before sanitization and accept it after, with non-finite floats
|
|
|
+// zeroed and finite ones preserved.
|
|
|
+func TestSanitizeNonFiniteFloatsAssetResponseMarshals(t *testing.T) {
|
|
|
+ usedBytes := math.NaN()
|
|
|
+ resp := &AssetResponse{
|
|
|
+ Assets: map[string]*AssetSet{
|
|
|
+ "assets": &AssetSet{
|
|
|
+ Name: "assets",
|
|
|
+ Assets: []*Asset{
|
|
|
+ &Asset{
|
|
|
+ Type: "Node",
|
|
|
+ Minutes: math.NaN(),
|
|
|
+ Adjustment: math.Inf(1),
|
|
|
+ TotalCost: math.Inf(-1),
|
|
|
+ CPUCost: math.NaN(),
|
|
|
+ GPUCost: 5.0, // finite, must be preserved
|
|
|
+ ByteHoursUsed: &usedBytes,
|
|
|
+ Overhead: &NodeOverhead{OverheadCostFraction: math.NaN()},
|
|
|
+ CPUBreakdown: &AssetBreakdown{Idle: math.NaN()},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := json.Marshal(resp); err == nil {
|
|
|
+ t.Fatal("expected json.Marshal to fail before sanitization (NaN/Inf present)")
|
|
|
+ }
|
|
|
+
|
|
|
+ resp = sanitizeNonFiniteFloats(resp).(*AssetResponse)
|
|
|
+
|
|
|
+ if _, err := json.Marshal(resp); err != nil {
|
|
|
+ t.Fatalf("expected json.Marshal to succeed after sanitization, got %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ a := resp.Assets["assets"].Assets[0]
|
|
|
+ if a.Minutes != 0 || a.Adjustment != 0 || a.TotalCost != 0 || a.CPUCost != 0 {
|
|
|
+ t.Fatalf("expected non-finite base floats zeroed, got %+v", a)
|
|
|
+ }
|
|
|
+ if a.GPUCost != 5.0 {
|
|
|
+ t.Fatalf("expected finite GPUCost preserved, got %v", a.GPUCost)
|
|
|
+ }
|
|
|
+ if a.ByteHoursUsed == nil || *a.ByteHoursUsed != 0 {
|
|
|
+ t.Fatalf("expected non-finite *float64 zeroed, got %v", a.ByteHoursUsed)
|
|
|
+ }
|
|
|
+ if a.Overhead.OverheadCostFraction != 0 {
|
|
|
+ t.Fatalf("expected nested overhead fraction zeroed, got %v", a.Overhead.OverheadCostFraction)
|
|
|
+ }
|
|
|
+ if a.CPUBreakdown.Idle != 0 {
|
|
|
+ t.Fatalf("expected nested breakdown value zeroed, got %v", a.CPUBreakdown.Idle)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestSanitizeNonFiniteFloatsValueType verifies a non-pointer (value) input is
|
|
|
+// sanitized via the returned copy, not just pointers.
|
|
|
+func TestSanitizeNonFiniteFloatsValueType(t *testing.T) {
|
|
|
+ in := Asset{TotalCost: math.NaN(), GPUCost: 3.0}
|
|
|
+ out, ok := sanitizeNonFiniteFloats(in).(Asset)
|
|
|
+ if !ok {
|
|
|
+ t.Fatalf("expected Asset back, got %T", sanitizeNonFiniteFloats(in))
|
|
|
+ }
|
|
|
+ if out.TotalCost != 0 {
|
|
|
+ t.Fatalf("expected NaN zeroed in returned value, got %v", out.TotalCost)
|
|
|
+ }
|
|
|
+ if out.GPUCost != 3.0 {
|
|
|
+ t.Fatalf("expected finite value preserved, got %v", out.GPUCost)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestSanitizeNonFiniteFloatsNilSafe(t *testing.T) {
|
|
|
+ if got := sanitizeNonFiniteFloats(nil); got != nil {
|
|
|
+ t.Fatalf("expected nil, got %v", got)
|
|
|
+ }
|
|
|
+ var p *AssetResponse
|
|
|
+ sanitizeNonFiniteFloats(p)
|
|
|
+ sanitizeNonFiniteFloats(&AssetResponse{})
|
|
|
+}
|