Files
Fabric/internal/server/ollama_test.go
Kayvan Sylvan ce74e881be fix: add validation for NaN, Inf, and negative values in parseOllamaNumCtx
## CHANGES

- Add NaN and Infinity validation for float64 values
- Add NaN and Infinity validation for float32 values
- Add negative value check for int64 type
- Add negative value check for json.Number type
- Add comprehensive test cases for special float values
- Add test cases for negative int64 and json.Number inputs
- Update line reference comments for validation checks
2026-01-18 07:42:10 -08:00

364 lines
8.3 KiB
Go

package restapi
import (
"encoding/json"
"math"
"strings"
"testing"
)
func TestBuildFabricChatURL(t *testing.T) {
tests := []struct {
name string
addr string
want string
wantErr bool
}{
{
name: "empty address",
addr: "",
want: "",
wantErr: true,
},
{
name: "valid http URL",
addr: "http://localhost:8080",
want: "http://localhost:8080",
wantErr: false,
},
{
name: "valid https URL",
addr: "https://api.example.com",
want: "https://api.example.com",
wantErr: false,
},
{
name: "http URL with trailing slash",
addr: "http://localhost:8080/",
want: "http://localhost:8080",
wantErr: false,
},
{
name: "malformed URL - missing host",
addr: "http://",
want: "",
wantErr: true,
},
{
name: "malformed URL - port only with http",
addr: "https://:8080",
want: "",
wantErr: true,
},
{
name: "colon-prefixed port",
addr: ":8080",
want: "http://127.0.0.1:8080",
wantErr: false,
},
{
name: "bare host:port",
addr: "localhost:8080",
want: "http://localhost:8080",
wantErr: false,
},
{
name: "bare hostname",
addr: "localhost",
want: "http://localhost",
wantErr: false,
},
{
name: "IP address with port",
addr: "192.168.1.1:3000",
want: "http://192.168.1.1:3000",
wantErr: false,
},
{
name: "bare address with path - invalid",
addr: "localhost:8080/some/path",
want: "",
wantErr: true,
},
{
name: "bare hostname with path - invalid",
addr: "localhost/api",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := buildFabricChatURL(tt.addr)
if (err != nil) != tt.wantErr {
t.Errorf("buildFabricChatURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("buildFabricChatURL() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseOllamaNumCtx(t *testing.T) {
tests := []struct {
name string
options map[string]any
want int
wantErr bool
errMsg string
}{
// --- Valid inputs ---
{
name: "nil options",
options: nil,
want: 0,
wantErr: false,
},
{
name: "empty options",
options: map[string]any{},
want: 0,
wantErr: false,
},
{
name: "num_ctx not present",
options: map[string]any{"other_key": 123},
want: 0,
wantErr: false,
},
{
name: "num_ctx is null",
options: map[string]any{"num_ctx": nil},
want: 0,
wantErr: false,
},
{
name: "valid int",
options: map[string]any{"num_ctx": 4096},
want: 4096,
wantErr: false,
},
{
name: "valid float64 (whole number)",
options: map[string]any{"num_ctx": float64(8192)},
want: 8192,
wantErr: false,
},
{
name: "valid float32 (whole number)",
options: map[string]any{"num_ctx": float32(2048)},
want: 2048,
wantErr: false,
},
{
name: "valid json.Number",
options: map[string]any{"num_ctx": json.Number("16384")},
want: 16384,
wantErr: false,
},
{
name: "valid string",
options: map[string]any{"num_ctx": "32768"},
want: 32768,
wantErr: false,
},
{
name: "valid int64",
options: map[string]any{"num_ctx": int64(65536)},
want: 65536,
wantErr: false,
},
// --- Invalid inputs ---
{
name: "float64 with fractional part",
options: map[string]any{"num_ctx": 4096.5},
want: 0,
wantErr: true,
errMsg: "num_ctx must be an integer, got float with fractional part",
},
{
name: "float32 with fractional part",
options: map[string]any{"num_ctx": float32(2048.75)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be an integer, got float with fractional part",
},
{
name: "negative int",
options: map[string]any{"num_ctx": -100},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
{
name: "zero int",
options: map[string]any{"num_ctx": 0},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
{
name: "negative float64",
options: map[string]any{"num_ctx": float64(-500)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
{
name: "negative float32",
options: map[string]any{"num_ctx": float32(-250)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
{
name: "non-numeric string",
options: map[string]any{"num_ctx": "not-a-number"},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a valid number",
},
{
name: "invalid json.Number",
options: map[string]any{"num_ctx": json.Number("invalid")},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a valid number",
},
{
name: "exceeds maximum allowed value",
options: map[string]any{"num_ctx": 2000000},
want: 0,
wantErr: true,
errMsg: "num_ctx exceeds maximum allowed value",
},
{
name: "unsupported type (bool)",
options: map[string]any{"num_ctx": true},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a number, got invalid type",
},
{
name: "unsupported type (slice)",
options: map[string]any{"num_ctx": []int{1, 2, 3}},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a number, got invalid type",
},
// --- Edge cases ---
{
name: "minimum valid value",
options: map[string]any{"num_ctx": 1},
want: 1,
wantErr: false,
},
{
name: "maximum allowed value",
options: map[string]any{"num_ctx": 1000000},
want: 1000000,
wantErr: false,
},
{
name: "very large float64 (overflow)",
options: map[string]any{"num_ctx": float64(math.MaxFloat64)},
want: 0,
wantErr: true,
errMsg: "num_ctx value out of range",
},
{
name: "large int64 exceeding maxInt on 32-bit",
options: map[string]any{"num_ctx": int64(1 << 40)},
want: 0,
wantErr: true,
errMsg: "num_ctx", // either "too large" or "exceeds maximum"
},
{
name: "long string gets truncated in error",
options: map[string]any{"num_ctx": "this-is-a-very-long-string-that-should-be-truncated-in-the-error-message"},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a valid number",
},
// --- Special float values ---
{
name: "float64 NaN",
options: map[string]any{"num_ctx": math.NaN()},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
{
name: "float64 positive infinity",
options: map[string]any{"num_ctx": math.Inf(1)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
{
name: "float64 negative infinity",
options: map[string]any{"num_ctx": math.Inf(-1)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
{
name: "float32 NaN",
options: map[string]any{"num_ctx": float32(math.NaN())},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
{
name: "float32 positive infinity",
options: map[string]any{"num_ctx": float32(math.Inf(1))},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
{
name: "float32 negative infinity",
options: map[string]any{"num_ctx": float32(math.Inf(-1))},
want: 0,
wantErr: true,
errMsg: "num_ctx must be a finite number",
},
// --- Negative int64 (32-bit wraparound prevention) ---
{
name: "negative int64",
options: map[string]any{"num_ctx": int64(-1000)},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
{
name: "negative json.Number",
options: map[string]any{"num_ctx": json.Number("-500")},
want: 0,
wantErr: true,
errMsg: "num_ctx must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseOllamaNumCtx(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("parseOllamaNumCtx() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errMsg != "" {
if !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("parseOllamaNumCtx() error message = %q, want to contain %q", err.Error(), tt.errMsg)
}
}
if got != tt.want {
t.Errorf("parseOllamaNumCtx() = %v, want %v", got, tt.want)
}
})
}
}