1package agent
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "sync/atomic"
9 "testing"
10
11 "github.com/patriceckhart/zot/packages/provider"
12)
13
14func TestLMStudioDiscoveryRequiresRegisteredURL(t *testing.T) {
15 t.Setenv("ZOT_HOME", t.TempDir())
16 provider.SetManagedModels(nil)
17 t.Cleanup(func() { provider.SetManagedModels(nil) })
18 var requests atomic.Int32
19 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 requests.Add(1)
21 fmt.Fprint(w, `{"models":[{"type":"llm","key":"local-model","max_context_length":8192}]}`)
22 }))
23 defer server.Close()
24 // Merely knowing an endpoint through an environment variable is not registration.
25 t.Setenv("LMSTUDIO_BASE_URL", server.URL)
26 if LMStudioConfigured() {
27 t.Fatal("unexpected default registration")
28 }
29 if err := RefreshLMStudioModels(context.Background()); err != nil {
30 t.Fatal(err)
31 }
32 if requests.Load() != 0 {
33 t.Fatal("unregistered discovery made a request")
34 }
35 if err := AuthStoreFor().SetAPIKey(provider.LMStudioProviderID, "key-only"); err != nil {
36 t.Fatal(err)
37 }
38 if err := RefreshLMStudioModels(context.Background()); err != nil {
39 t.Fatal(err)
40 }
41 if requests.Load() != 0 {
42 t.Fatal("key without URL enabled discovery")
43 }
44 if err := AuthStoreFor().SetEndpointCredential(provider.LMStudioProviderID, server.URL, ""); err != nil {
45 t.Fatal(err)
46 }
47 if !LMStudioConfigured() {
48 t.Fatal("missing registration")
49 }
50 if err := RefreshLMStudioModels(context.Background()); err != nil {
51 t.Fatal(err)
52 }
53 if requests.Load() != 1 {
54 t.Fatalf("requests=%d", requests.Load())
55 }
56 m, err := provider.FindModel(provider.LMStudioProviderID, "local-model")
57 if err != nil || m.ContextWindow != 8192 {
58 t.Fatalf("model=%+v, err=%v", m, err)
59 }
60 r, err := Resolve(Args{Provider: provider.LMStudioProviderID, Model: "local-model", NoSkill: true}, true)
61 if err != nil {
62 t.Fatal(err)
63 }
64 if r.BaseURL != server.URL+"/v1" || r.Credential != "local" {
65 t.Fatalf("incorrect local routing: %s", r.BaseURL)
66 }
67 if err := AuthStoreFor().Clear(provider.LMStudioProviderID); err != nil {
68 t.Fatal(err)
69 }
70 if err := RefreshLMStudioModels(context.Background()); err != nil {
71 t.Fatal(err)
72 }
73 if requests.Load() != 2 {
74 t.Fatal("logout still fetched models")
75 }
76 if _, err := provider.FindModel(provider.LMStudioProviderID, "local-model"); err == nil {
77 t.Fatal("logout retained discovered model")
78 }
79}
80
81func TestLMStudioConfigSurvivesTransientCatalog(t *testing.T) {
82 t.Setenv("ZOT_HOME", t.TempDir())
83 provider.SetManagedModels(nil)
84 t.Cleanup(func() { provider.SetManagedModels(nil) })
85 if err := SaveConfig(Config{Provider: provider.LMStudioProviderID, Model: "gpt-5"}); err != nil {
86 t.Fatal(err)
87 }
88 ValidateAndRepairConfig()
89 cfg, err := LoadConfig()
90 if err != nil || cfg.Provider != provider.LMStudioProviderID || cfg.Model != "gpt-5" {
91 t.Fatalf("config=%+v error=%v", cfg, err)
92 }
93}
94
95func TestLMStudioRefreshPreservesSnapshotOnFailure(t *testing.T) {
96 t.Setenv("ZOT_HOME", t.TempDir())
97 provider.SetManagedModels([]provider.Model{{Provider: provider.LMStudioProviderID, ID: "previous", BaseURL: "http://unused.invalid/v1", Source: "live"}})
98 t.Cleanup(func() { provider.SetManagedModels(nil) })
99 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(503) }))
100 defer server.Close()
101 if err := AuthStoreFor().SetEndpointCredential(provider.LMStudioProviderID, server.URL, ""); err != nil {
102 t.Fatal(err)
103 }
104 if err := RefreshLMStudioModels(context.Background()); err == nil {
105 t.Fatal("missing discovery error")
106 }
107 if _, err := provider.FindModel(provider.LMStudioProviderID, "previous"); err != nil {
108 t.Fatal(err)
109 }
110 r, err := Resolve(Args{Provider: provider.LMStudioProviderID, Model: "previous", NoSkill: true}, true)
111 if err != nil {
112 t.Fatal(err)
113 }
114 if r.BaseURL != server.URL+"/v1" {
115 t.Fatalf("stale endpoint used for inference: %s", r.BaseURL)
116 }
117}