* fix: add retry logic and fallback for checksums.txt download - Add HTTP client with 30s timeout to ReleaseManager - Implement downloadFileWithRetry with 3 attempts and exponential backoff - Allow manual checksum placement at ~/.localai/checksums/checksums-<version>.txt - Continue installation with warning if checksum download/verification fails - Add test for HTTPClient initialization - Fix linter error in systray_manager.go Fixes #7385 Signed-off-by: majiayu000 <1835304752@qq.com> * fix: add retry logic and improve checksums.txt download handling This commit addresses issue #7385 by implementing: - Retry logic (3 attempts) for checksum file downloads - Fallback to manually placed checksum files - Option to proceed with installation if checksums unavailable (with warnings) - Fixed resource leaks in download retry loop - Added configurable HTTP client with 30s timeout The installation will now be more resilient to network issues while maintaining security through checksum verification when available. Signed-off-by: majiayu000 <1835304752@qq.com> * fix: check for existing checksum file before downloading This commit addresses the review feedback from mudler on PR #7788. The code now checks if there's already a checksum file (either manually placed or previously downloaded) and honors that, skipping download entirely in such case. Changes: - Check for existing checksum file at ~/.localai/checksums/checksums-<version>.txt first - Check for existing downloaded checksum file at binary path - Only attempt to download if no existing checksum file is found - This prevents unnecessary network requests and honors user-placed checksums Signed-off-by: majiayu000 <1835304752@qq.com> 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
package launcher_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
launcher "github.com/mudler/LocalAI/cmd/launcher/internal"
|
|
)
|
|
|
|
var _ = Describe("ReleaseManager", func() {
|
|
var (
|
|
rm *launcher.ReleaseManager
|
|
tempDir string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
tempDir, err = os.MkdirTemp("", "launcher-test-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
rm = launcher.NewReleaseManager()
|
|
// Override binary path for testing
|
|
rm.BinaryPath = tempDir
|
|
})
|
|
|
|
AfterEach(func() {
|
|
os.RemoveAll(tempDir)
|
|
})
|
|
|
|
Describe("NewReleaseManager", func() {
|
|
It("should create a release manager with correct defaults", func() {
|
|
newRM := launcher.NewReleaseManager()
|
|
Expect(newRM.GitHubOwner).To(Equal("mudler"))
|
|
Expect(newRM.GitHubRepo).To(Equal("LocalAI"))
|
|
Expect(newRM.BinaryPath).To(ContainSubstring(".localai"))
|
|
Expect(newRM.HTTPClient).ToNot(BeNil())
|
|
Expect(newRM.HTTPClient.Timeout).To(Equal(30 * time.Second))
|
|
})
|
|
})
|
|
|
|
Describe("GetBinaryName", func() {
|
|
It("should return correct binary name for current platform", func() {
|
|
binaryName := rm.GetBinaryName("v3.4.0")
|
|
expectedOS := runtime.GOOS
|
|
expectedArch := runtime.GOARCH
|
|
|
|
expected := "local-ai-v3.4.0-" + expectedOS + "-" + expectedArch
|
|
Expect(binaryName).To(Equal(expected))
|
|
})
|
|
|
|
It("should handle version with and without 'v' prefix", func() {
|
|
withV := rm.GetBinaryName("v3.4.0")
|
|
withoutV := rm.GetBinaryName("3.4.0")
|
|
|
|
// Both should produce the same result
|
|
Expect(withV).To(Equal(withoutV))
|
|
})
|
|
})
|
|
|
|
Describe("GetBinaryPath", func() {
|
|
It("should return the correct binary path", func() {
|
|
path := rm.GetBinaryPath()
|
|
expected := filepath.Join(tempDir, "local-ai")
|
|
Expect(path).To(Equal(expected))
|
|
})
|
|
})
|
|
|
|
Describe("GetInstalledVersion", func() {
|
|
It("should return empty when no binary exists", func() {
|
|
version := rm.GetInstalledVersion()
|
|
Expect(version).To(BeEmpty()) // No binary installed in test
|
|
})
|
|
|
|
It("should return empty version when binary exists but no metadata", func() {
|
|
// Create a fake binary for testing
|
|
err := os.MkdirAll(rm.BinaryPath, 0755)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
binaryPath := rm.GetBinaryPath()
|
|
err = os.WriteFile(binaryPath, []byte("fake binary"), 0755)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
version := rm.GetInstalledVersion()
|
|
Expect(version).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Context("with mocked responses", func() {
|
|
// Note: In a real implementation, we'd mock HTTP responses
|
|
// For now, we'll test the structure and error handling
|
|
|
|
Describe("GetLatestRelease", func() {
|
|
It("should handle network errors gracefully", func() {
|
|
// This test would require mocking HTTP client
|
|
// For demonstration, we're just testing the method exists
|
|
_, err := rm.GetLatestRelease()
|
|
// We expect either success or a network error, not a panic
|
|
// In a real test, we'd mock the HTTP response
|
|
if err != nil {
|
|
Expect(err.Error()).To(ContainSubstring("failed to fetch"))
|
|
}
|
|
})
|
|
})
|
|
|
|
Describe("DownloadRelease", func() {
|
|
It("should create binary directory if it doesn't exist", func() {
|
|
// Remove the temp directory to test creation
|
|
os.RemoveAll(tempDir)
|
|
|
|
// This will fail due to network, but should create the directory
|
|
rm.DownloadRelease("v3.4.0", nil)
|
|
|
|
// Check if directory was created
|
|
_, err := os.Stat(tempDir)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("VerifyChecksum functionality", func() {
|
|
var (
|
|
testFile string
|
|
checksumFile string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
testFile = filepath.Join(tempDir, "test-binary")
|
|
checksumFile = filepath.Join(tempDir, "checksums.txt")
|
|
})
|
|
|
|
It("should verify checksums correctly", func() {
|
|
// Create a test file with known content
|
|
testContent := []byte("test content for checksum")
|
|
err := os.WriteFile(testFile, testContent, 0644)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Calculate expected SHA256
|
|
// This is a simplified test - in practice we'd use the actual checksum
|
|
checksumContent := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 test-binary\n"
|
|
err = os.WriteFile(checksumFile, []byte(checksumContent), 0644)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Test checksum verification
|
|
// Note: This will fail because our content doesn't match the empty string hash
|
|
// In a real test, we'd calculate the actual hash
|
|
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
|
// We expect this to fail since we're using a dummy checksum
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("checksum mismatch"))
|
|
})
|
|
|
|
It("should handle missing checksum file", func() {
|
|
// Create test file but no checksum file
|
|
err := os.WriteFile(testFile, []byte("test"), 0644)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("failed to open checksums file"))
|
|
})
|
|
|
|
It("should handle missing binary in checksums", func() {
|
|
// Create files but checksum doesn't contain our binary
|
|
err := os.WriteFile(testFile, []byte("test"), 0644)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
checksumContent := "hash other-binary\n"
|
|
err = os.WriteFile(checksumFile, []byte(checksumContent), 0644)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = rm.VerifyChecksum(testFile, checksumFile, "test-binary")
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("checksum not found"))
|
|
})
|
|
})
|
|
})
|