package envconfig import ( "log/slog" "math" "os" "path/filepath" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/ollama/ollama/logutil" ) func TestHost(t *testing.T) { cases := map[string]struct { value string expect string }{ "empty": {"", "http://127.0.0.1:11434"}, "only address": {"1.2.3.4", "http://1.2.3.4:11434"}, "only port": {":1234", "http://:1234"}, "address and port": {"1.2.3.4:1234", "http://1.2.3.4:1234"}, "hostname": {"example.com", "http://example.com:11434"}, "hostname and port": {"example.com:1234", "http://example.com:1234"}, "zero port": {":0", "http://:0"}, "too large port": {":66000", "http://:11434"}, "too small port": {":-1", "http://:11434"}, "ipv6 localhost": {"[::1]", "http://[::1]:11434"}, "ipv6 world open": {"[::]", "http://[::]:11434"}, "ipv6 no brackets": {"::1", "http://[::1]:11434"}, "ipv6 + port": {"[::1]:1337", "http://[::1]:1337"}, "extra space": {" 1.2.3.4 ", "http://1.2.3.4:11434"}, "extra quotes": {"\"1.2.3.4\"", "http://1.2.3.4:11434"}, "extra space+quotes": {" \" 1.2.3.4 \" ", "http://1.2.3.4:11434"}, "extra single quotes": {"'1.2.3.4'", "http://1.2.3.4:11434"}, "http": {"http://1.2.3.4", "http://1.2.3.4:80"}, "http port": {"http://1.2.3.4:4321", "http://1.2.3.4:4321"}, "https": {"https://1.2.3.4", "https://1.2.3.4:443"}, "https port": {"https://1.2.3.4:4321", "https://1.2.3.4:4321"}, "proxy path": {"https://example.com/ollama", "https://example.com:443/ollama"}, "ollama.com": {"ollama.com", "https://ollama.com:443"}, } for name, tt := range cases { t.Run(name, func(t *testing.T) { t.Setenv("OLLAMA_HOST", tt.value) if host := Host(); host.String() != tt.expect { t.Errorf("%s: expected %s, got %s", name, tt.expect, host.String()) } }) } } func TestConnectableHost(t *testing.T) { cases := map[string]struct { value string expect string }{ "empty": {"", "http://127.0.0.1:11434"}, "localhost": {"127.0.0.1", "http://127.0.0.1:11434"}, "localhost and port": {"127.0.0.1:1234", "http://127.0.0.1:1234"}, "ipv4 unspecified": {"0.0.0.0", "http://127.0.0.1:11434"}, "ipv4 unspecified + port": {"0.0.0.0:1234", "http://127.0.0.1:1234"}, "ipv6 unspecified": {"[::]", "http://[::1]:11434"}, "ipv6 unspecified + port": {"[::]:1234", "http://[::1]:1234"}, "ipv6 localhost": {"[::1]", "http://[::1]:11434"}, "ipv6 localhost + port": {"[::1]:1234", "http://[::1]:1234"}, "specific address": {"192.168.1.5", "http://192.168.1.5:11434"}, "specific address + port": {"192.168.1.5:8080", "http://192.168.1.5:8080"}, "hostname": {"example.com", "http://example.com:11434"}, "hostname and port": {"example.com:1234", "http://example.com:1234"}, "https unspecified + port": {"https://0.0.0.0:4321", "https://127.0.0.1:4321"}, } for name, tt := range cases { t.Run(name, func(t *testing.T) { t.Setenv("OLLAMA_HOST", tt.value) if host := ConnectableHost(); host.String() != tt.expect { t.Errorf("%s: expected %s, got %s", name, tt.expect, host.String()) } }) } } func TestOrigins(t *testing.T) { cases := []struct { value string expect []string }{ {"", []string{ "http://localhost", "https://localhost", "http://localhost:*", "https://localhost:*", "http://127.0.0.1", "https://127.0.0.1", "http://127.0.0.1:*", "https://127.0.0.1:*", "http://0.0.0.0", "https://0.0.0.0", "http://0.0.0.0:*", "https://0.0.0.0:*", "app://*", "file://*", "tauri://*", "vscode-webview://*", "vscode-file://*", }}, {"http://10.0.0.1", []string{ "http://10.0.0.1", "http://localhost", "https://localhost", "http://localhost:*", "https://localhost:*", "http://127.0.0.1", "https://127.0.0.1", "http://127.0.0.1:*", "https://127.0.0.1:*", "http://0.0.0.0", "https://0.0.0.0", "http://0.0.0.0:*", "https://0.0.0.0:*", "app://*", "file://*", "tauri://*", "vscode-webview://*", "vscode-file://*", }}, {"http://172.16.0.1,https://192.168.0.1", []string{ "http://172.16.0.1", "https://192.168.0.1", "http://localhost", "https://localhost", "http://localhost:*", "https://localhost:*", "http://127.0.0.1", "https://127.0.0.1", "http://127.0.0.1:*", "https://127.0.0.1:*", "http://0.0.0.0", "https://0.0.0.0", "http://0.0.0.0:*", "https://0.0.0.0:*", "app://*", "file://*", "tauri://*", "vscode-webview://*", "vscode-file://*", }}, {"http://totally.safe,http://definitely.legit", []string{ "http://totally.safe", "http://definitely.legit", "http://localhost", "https://localhost", "http://localhost:*", "https://localhost:*", "http://127.0.0.1", "https://127.0.0.1", "http://127.0.0.1:*", "https://127.0.0.1:*", "http://0.0.0.0", "https://0.0.0.0", "http://0.0.0.0:*", "https://0.0.0.0:*", "app://*", "file://*", "tauri://*", "vscode-webview://*", "vscode-file://*", }}, } for _, tt := range cases { t.Run(tt.value, func(t *testing.T) { t.Setenv("OLLAMA_ORIGINS", tt.value) if diff := cmp.Diff(AllowedOrigins(), tt.expect); diff != "" { t.Errorf("%s: mismatch (-want +got):\n%s", tt.value, diff) } }) } } func TestBool(t *testing.T) { cases := map[string]bool{ "": false, "true": true, "false": false, "1": true, "0": false, // invalid values "random": true, "something": true, } for k, v := range cases { t.Run(k, func(t *testing.T) { t.Setenv("OLLAMA_BOOL", k) if b := Bool("OLLAMA_BOOL")(); b != v { t.Errorf("%s: expected %t, got %t", k, v, b) } }) } } func TestUint(t *testing.T) { cases := map[string]uint{ "0": 0, "1": 1, "1337": 1337, // default values "": 11434, "-1": 11434, "0o10": 11434, "0x10": 11434, "string": 11434, } for k, v := range cases { t.Run(k, func(t *testing.T) { t.Setenv("OLLAMA_UINT", k) if i := Uint("OLLAMA_UINT", 11434)(); i != v { t.Errorf("%s: expected %d, got %d", k, v, i) } }) } } func TestKeepAlive(t *testing.T) { cases := map[string]time.Duration{ "": 5 * time.Minute, "1s": time.Second, "1m": time.Minute, "1h": time.Hour, "5m0s": 5 * time.Minute, "1h2m3s": 1*time.Hour + 2*time.Minute + 3*time.Second, "0": time.Duration(0), "60": 60 * time.Second, "120": 2 * time.Minute, "3600": time.Hour, "-0": time.Duration(0), "-1": time.Duration(math.MaxInt64), "-1m": time.Duration(math.MaxInt64), // invalid values " ": 5 * time.Minute, "???": 5 * time.Minute, "1d": 5 * time.Minute, "1y": 5 * time.Minute, "1w": 5 * time.Minute, } for tt, expect := range cases { t.Run(tt, func(t *testing.T) { t.Setenv("OLLAMA_KEEP_ALIVE", tt) if actual := KeepAlive(); actual != expect { t.Errorf("%s: expected %s, got %s", tt, expect, actual) } }) } } func TestLoadTimeout(t *testing.T) { defaultTimeout := 5 * time.Minute cases := map[string]time.Duration{ "": defaultTimeout, "1s": time.Second, "1m": time.Minute, "1h": time.Hour, "5m0s": defaultTimeout, "1h2m3s": 1*time.Hour + 2*time.Minute + 3*time.Second, "0": time.Duration(math.MaxInt64), "60": 60 * time.Second, "120": 2 * time.Minute, "3600": time.Hour, "-0": time.Duration(math.MaxInt64), "-1": time.Duration(math.MaxInt64), "-1m": time.Duration(math.MaxInt64), // invalid values " ": defaultTimeout, "???": defaultTimeout, "1d": defaultTimeout, "1y": defaultTimeout, "1w": defaultTimeout, } for tt, expect := range cases { t.Run(tt, func(t *testing.T) { t.Setenv("OLLAMA_LOAD_TIMEOUT", tt) if actual := LoadTimeout(); actual != expect { t.Errorf("%s: expected %s, got %s", tt, expect, actual) } }) } } func TestVar(t *testing.T) { cases := map[string]string{ "value": "value", " value ": "value", " 'value' ": "value", ` "value" `: "value", " ' value ' ": " value ", ` " value " `: " value ", } for k, v := range cases { t.Run(k, func(t *testing.T) { t.Setenv("OLLAMA_VAR", k) if s := Var("OLLAMA_VAR"); s != v { t.Errorf("%s: expected %q, got %q", k, v, s) } }) } } func TestContextLength(t *testing.T) { cases := map[string]uint{ "": 0, "2048": 2048, } for k, v := range cases { t.Run(k, func(t *testing.T) { t.Setenv("OLLAMA_CONTEXT_LENGTH", k) if i := ContextLength(); i != v { t.Errorf("%s: expected %d, got %d", k, v, i) } }) } } func TestLogLevel(t *testing.T) { cases := map[string]slog.Level{ // Default to INFO "": slog.LevelInfo, "false": slog.LevelInfo, "f": slog.LevelInfo, "0": slog.LevelInfo, // True values enable Debug "true": slog.LevelDebug, "t": slog.LevelDebug, // Positive values increase verbosity "1": slog.LevelDebug, "2": logutil.LevelTrace, // Negative values decrease verbosity "-1": slog.LevelWarn, "-2": slog.LevelError, } for k, v := range cases { t.Run(k, func(t *testing.T) { t.Setenv("OLLAMA_DEBUG", k) if i := LogLevel(); i != v { t.Errorf("%s: expected %d, got %d", k, v, i) } }) } } func TestNoCloud(t *testing.T) { tests := []struct { name string envValue string configContent string wantDisabled bool wantSource string }{ { name: "neither env nor config", wantDisabled: false, wantSource: "none", }, { name: "env only", envValue: "1", wantDisabled: true, wantSource: "env", }, { name: "config only", configContent: `{"disable_ollama_cloud": true}`, wantDisabled: true, wantSource: "config", }, { name: "both env and config", envValue: "1", configContent: `{"disable_ollama_cloud": true}`, wantDisabled: true, wantSource: "both", }, { name: "config false", configContent: `{"disable_ollama_cloud": false}`, wantDisabled: false, wantSource: "none", }, { name: "invalid config ignored", configContent: `{invalid json`, wantDisabled: false, wantSource: "none", }, { name: "no config file", wantDisabled: false, wantSource: "none", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { home := t.TempDir() if tt.configContent != "" { configDir := filepath.Join(home, ".ollama") if err := os.MkdirAll(configDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(configDir, "server.json"), []byte(tt.configContent), 0o644); err != nil { t.Fatal(err) } } setTestHome(t, home) t.Setenv("OLLAMA_NO_CLOUD", tt.envValue) if got := NoCloud(); got != tt.wantDisabled { t.Errorf("NoCloud() = %v, want %v", got, tt.wantDisabled) } if got := NoCloudSource(); got != tt.wantSource { t.Errorf("NoCloudSource() = %q, want %q", got, tt.wantSource) } }) } }