@@ -20,6 +20,7 @@ import (
2020 "github.com/mudler/LocalAI/pkg/system"
2121 . "github.com/onsi/ginkgo/v2"
2222 . "github.com/onsi/gomega"
23+ "gopkg.in/yaml.v3"
2324)
2425
2526func TestRoutes (t * testing.T ) {
@@ -176,6 +177,161 @@ var _ = Describe("Backend API Routes", func() {
176177 Expect (response ["processed" ]).To (Equal (false ))
177178 })
178179 })
180+
181+ Describe ("Backend upgrade API" , func () {
182+ var (
183+ galleryFile string
184+ upgradeApp * echo.Echo
185+ upgradeGallerySvc * galleryop.GalleryService
186+ )
187+
188+ BeforeEach (func () {
189+ // Place gallery file inside backends dir so it passes trusted root checks
190+ galleryFile = filepath .Join (systemState .Backend .BackendsPath , "test-gallery.yaml" )
191+
192+ // Create a fake "v1" backend on disk (simulates a previously installed backend)
193+ backendDir := filepath .Join (systemState .Backend .BackendsPath , "test-upgrade-backend" )
194+ err := os .MkdirAll (backendDir , 0750 )
195+ Expect (err ).NotTo (HaveOccurred ())
196+ err = os .WriteFile (filepath .Join (backendDir , "run.sh" ), []byte ("#!/bin/sh\n echo v1" ), 0755 )
197+ Expect (err ).NotTo (HaveOccurred ())
198+
199+ // Write metadata.json for the installed backend (v1)
200+ metadata := map [string ]string {
201+ "name" : "test-upgrade-backend" ,
202+ "version" : "1.0.0" ,
203+ "installed_at" : "2024-01-01T00:00:00Z" ,
204+ }
205+ metadataBytes , err := json .MarshalIndent (metadata , "" , " " )
206+ Expect (err ).NotTo (HaveOccurred ())
207+ err = os .WriteFile (filepath .Join (backendDir , "metadata.json" ), metadataBytes , 0644 )
208+ Expect (err ).NotTo (HaveOccurred ())
209+
210+ // Create a "v2" source directory (the upgrade target)
211+ // Must be inside backends path to pass trusted root checks
212+ v2SrcDir := filepath .Join (systemState .Backend .BackendsPath , "v2-backend-src" )
213+ err = os .MkdirAll (v2SrcDir , 0750 )
214+ Expect (err ).NotTo (HaveOccurred ())
215+ err = os .WriteFile (filepath .Join (v2SrcDir , "run.sh" ), []byte ("#!/bin/sh\n echo v2" ), 0755 )
216+ Expect (err ).NotTo (HaveOccurred ())
217+
218+ // Write gallery YAML pointing to v2
219+ galleryData := []map [string ]any {
220+ {
221+ "name" : "test-upgrade-backend" ,
222+ "uri" : v2SrcDir ,
223+ "version" : "2.0.0" ,
224+ },
225+ }
226+ yamlBytes , err := yaml .Marshal (galleryData )
227+ Expect (err ).NotTo (HaveOccurred ())
228+ err = os .WriteFile (galleryFile , yamlBytes , 0644 )
229+ Expect (err ).NotTo (HaveOccurred ())
230+
231+ // Configure the gallery in appConfig BEFORE creating the gallery service
232+ // so the backend manager captures the correct galleries
233+ appConfig .BackendGalleries = []config.Gallery {
234+ {Name : "test" , URL : "file://" + galleryFile },
235+ }
236+
237+ // Create a fresh gallery service with the upgrade gallery configured
238+ upgradeGallerySvc = galleryop .NewGalleryService (appConfig , modelLoader )
239+ err = upgradeGallerySvc .Start (context .Background (), configLoader , systemState )
240+ Expect (err ).NotTo (HaveOccurred ())
241+
242+ // Register routes with the upgrade-aware gallery service
243+ upgradeApp = echo .New ()
244+ opcache := galleryop .NewOpCache (upgradeGallerySvc )
245+ noopMw := func (next echo.HandlerFunc ) echo.HandlerFunc { return next }
246+ routes .RegisterUIAPIRoutes (upgradeApp , configLoader , modelLoader , appConfig , upgradeGallerySvc , opcache , nil , noopMw )
247+ })
248+
249+ Describe ("GET /api/backends/upgrades" , func () {
250+ It ("should return available upgrades" , func () {
251+ req := httptest .NewRequest (http .MethodGet , "/api/backends/upgrades" , nil )
252+ rec := httptest .NewRecorder ()
253+
254+ upgradeApp .ServeHTTP (rec , req )
255+
256+ Expect (rec .Code ).To (Equal (http .StatusOK ))
257+
258+ var response map [string ]any
259+ err := json .Unmarshal (rec .Body .Bytes (), & response )
260+ Expect (err ).NotTo (HaveOccurred ())
261+ // Response is empty (upgrade checker not running in test),
262+ // but the endpoint should not error
263+ })
264+ })
265+
266+ Describe ("POST /api/backends/upgrade/:name" , func () {
267+ It ("should accept upgrade request and return job ID" , func () {
268+ req := httptest .NewRequest (http .MethodPost , "/api/backends/upgrade/test-upgrade-backend" , nil )
269+ req .Header .Set ("Content-Type" , "application/json" )
270+ rec := httptest .NewRecorder ()
271+
272+ upgradeApp .ServeHTTP (rec , req )
273+
274+ Expect (rec .Code ).To (Equal (http .StatusOK ))
275+
276+ var response map [string ]any
277+ err := json .Unmarshal (rec .Body .Bytes (), & response )
278+ Expect (err ).NotTo (HaveOccurred ())
279+ Expect (response ["uuid" ]).NotTo (BeEmpty ())
280+ Expect (response ["statusUrl" ]).NotTo (BeEmpty ())
281+ })
282+
283+ It ("should upgrade the backend and update metadata" , func () {
284+ req := httptest .NewRequest (http .MethodPost , "/api/backends/upgrade/test-upgrade-backend" , nil )
285+ req .Header .Set ("Content-Type" , "application/json" )
286+ rec := httptest .NewRecorder ()
287+
288+ upgradeApp .ServeHTTP (rec , req )
289+ Expect (rec .Code ).To (Equal (http .StatusOK ))
290+
291+ var response map [string ]any
292+ err := json .Unmarshal (rec .Body .Bytes (), & response )
293+ Expect (err ).NotTo (HaveOccurred ())
294+ jobID := response ["uuid" ].(string )
295+
296+ // Wait for the upgrade job to complete
297+ Eventually (func () bool {
298+ jobReq := httptest .NewRequest (http .MethodGet , "/api/backends/job/" + jobID , nil )
299+ jobRec := httptest .NewRecorder ()
300+ upgradeApp .ServeHTTP (jobRec , jobReq )
301+
302+ var jobResp map [string ]any
303+ json .Unmarshal (jobRec .Body .Bytes (), & jobResp )
304+
305+ processed , _ := jobResp ["processed" ].(bool )
306+ return processed
307+ }, "10s" , "200ms" ).Should (BeTrue ())
308+
309+ // Verify the backend was upgraded: run.sh should now contain "v2"
310+ runContent , err := os .ReadFile (filepath .Join (
311+ systemState .Backend .BackendsPath , "test-upgrade-backend" , "run.sh" ))
312+ Expect (err ).NotTo (HaveOccurred ())
313+ Expect (string (runContent )).To (ContainSubstring ("v2" ))
314+
315+ // Verify metadata was updated with new version
316+ metadataContent , err := os .ReadFile (filepath .Join (
317+ systemState .Backend .BackendsPath , "test-upgrade-backend" , "metadata.json" ))
318+ Expect (err ).NotTo (HaveOccurred ())
319+ Expect (string (metadataContent )).To (ContainSubstring (`"version": "2.0.0"` ))
320+ })
321+ })
322+
323+ Describe ("POST /api/backends/upgrades/check" , func () {
324+ It ("should trigger an upgrade check and return 200" , func () {
325+ req := httptest .NewRequest (http .MethodPost , "/api/backends/upgrades/check" , nil )
326+ req .Header .Set ("Content-Type" , "application/json" )
327+ rec := httptest .NewRecorder ()
328+
329+ upgradeApp .ServeHTTP (rec , req )
330+
331+ Expect (rec .Code ).To (Equal (http .StatusOK ))
332+ })
333+ })
334+ })
179335})
180336
181337// Helper function to make POST request
0 commit comments