Files
wehub-resource-sync 1b8708893a
Security Scan / tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:12:26 +08:00

108 lines
4.1 KiB
Go

package galleryop_test
import (
"errors"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/galleryop"
)
// These specs reproduce the distributed "Reinstall spins forever" bug:
// processingBackends (the UI spinner source) is built from OpCache.GetStatus,
// which historically returned every cached op unconditionally. Cleanup only
// happened when a client polled /api/backends/job/:uid, but the Manage-page
// Reinstall/Upgrade buttons never poll, so a completed install stayed in
// processingBackends forever. GetStatus must self-evict terminal ops.
var _ = Describe("OpCache.GetStatus eviction", func() {
var (
svc *galleryop.GalleryService
cache *galleryop.OpCache
)
BeforeEach(func() {
svc = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
cache = galleryop.NewOpCache(svc)
})
It("keeps an op that is still processing", func() {
cache.SetBackend("llama-cpp", "uuid-1")
svc.UpdateStatus("uuid-1", &galleryop.OpStatus{Message: "processing backend: llama-cpp", Progress: 0})
processing, _ := cache.GetStatus()
Expect(processing).To(HaveKeyWithValue("llama-cpp", "uuid-1"))
Expect(cache.Exists("llama-cpp")).To(BeTrue())
})
It("evicts a completed op so it no longer shows as processing", func() {
cache.SetBackend("llama-cpp", "uuid-1")
svc.UpdateStatus("uuid-1", &galleryop.OpStatus{Processed: true, Progress: 100, Message: "completed"})
processing, _ := cache.GetStatus()
Expect(processing).NotTo(HaveKey("llama-cpp"))
Expect(cache.Exists("llama-cpp")).To(BeFalse())
})
It("keeps a failed op so the operations panel can surface the error and offer Dismiss", func() {
cache.SetBackend("piper", "uuid-2")
svc.UpdateStatus("uuid-2", &galleryop.OpStatus{Processed: true, Error: errors.New("boom")})
processing, _ := cache.GetStatus()
Expect(processing).To(HaveKeyWithValue("piper", "uuid-2"))
Expect(cache.Exists("piper")).To(BeTrue())
})
It("keeps the ErrWorkerStillInstalling soft-path op (worker still installing in background)", func() {
// Processed=true with no error but progress != 100 and a non-"completed"
// message: the worker timed out the NATS round-trip but is still installing.
// Evicting it would hide an install that may still fail; the reconciler
// confirms the real outcome later.
cache.SetBackend("vllm-development", "uuid-soft")
svc.UpdateStatus("uuid-soft", &galleryop.OpStatus{
Processed: true,
Message: "backend vllm-development: worker still installing in background; reconciler will confirm completion",
})
processing, _ := cache.GetStatus()
Expect(processing).To(HaveKeyWithValue("vllm-development", "uuid-soft"))
Expect(cache.Exists("vllm-development")).To(BeTrue())
})
It("evicts a cancelled op", func() {
cache.SetBackend("vllm", "uuid-3")
svc.UpdateStatus("uuid-3", &galleryop.OpStatus{Processed: true, Cancelled: true, Message: "cancelled"})
processing, _ := cache.GetStatus()
Expect(processing).NotTo(HaveKey("vllm"))
})
It("does not evict an op with no status yet (queued)", func() {
cache.SetBackend("whisper", "uuid-4")
processing, taskTypes := cache.GetStatus()
Expect(processing).To(HaveKeyWithValue("whisper", "uuid-4"))
Expect(taskTypes).To(HaveKeyWithValue("whisper", "Waiting"))
})
// Regression guard: GetStatus is called concurrently by four HTTP handlers
// (~1s poll). An earlier version evicted by deleting from m.Map() — which
// returns the live internal map by reference — causing a fatal
// "concurrent map writes" crash. Run under -race; must not panic or race.
It("is safe under concurrent GetStatus + Set/complete", func() {
done := make(chan struct{})
go func() {
defer GinkgoRecover()
for i := 0; i < 2000; i++ {
_, _ = cache.GetStatus()
}
close(done)
}()
for i := 0; i < 2000; i++ {
id := "uuid-c"
cache.SetBackend("concurrent-backend", id)
// Half the time mark it completed so GetStatus evicts it.
if i%2 == 0 {
svc.UpdateStatus(id, &galleryop.OpStatus{Processed: true, Progress: 100, Message: "completed"})
}
_, _ = cache.GetStatus()
}
<-done
})
})