| | package hfapi_test |
| |
|
| | import ( |
| | "fmt" |
| | "net/http" |
| | "net/http/httptest" |
| | "strings" |
| |
|
| | . "github.com/onsi/ginkgo/v2" |
| | . "github.com/onsi/gomega" |
| |
|
| | hfapi "github.com/mudler/LocalAI/pkg/huggingface-api" |
| | ) |
| |
|
| | var _ = Describe("HuggingFace API Client", func() { |
| | var ( |
| | client *hfapi.Client |
| | server *httptest.Server |
| | ) |
| |
|
| | BeforeEach(func() { |
| | client = hfapi.NewClient() |
| | }) |
| |
|
| | AfterEach(func() { |
| | if server != nil { |
| | server.Close() |
| | } |
| | }) |
| |
|
| | Context("when creating a new client", func() { |
| | It("should initialize with correct base URL", func() { |
| | Expect(client).ToNot(BeNil()) |
| | Expect(client.BaseURL()).To(Equal("https://huggingface.co/api/models")) |
| | }) |
| | }) |
| |
|
| | Context("when searching for models", func() { |
| | BeforeEach(func() { |
| | |
| | mockResponse := `[ |
| | { |
| | "modelId": "test-model-1", |
| | "author": "test-author", |
| | "downloads": 1000, |
| | "lastModified": "2024-01-01T00:00:00.000Z", |
| | "pipelineTag": "text-generation", |
| | "private": false, |
| | "tags": ["gguf", "llama"], |
| | "createdAt": "2024-01-01T00:00:00.000Z", |
| | "updatedAt": "2024-01-01T00:00:00.000Z", |
| | "sha": "abc123", |
| | "config": {}, |
| | "model_index": "test-index", |
| | "library_name": "transformers", |
| | "mask_token": null, |
| | "tokenizer_class": "LlamaTokenizer" |
| | }, |
| | { |
| | "modelId": "test-model-2", |
| | "author": "test-author-2", |
| | "downloads": 2000, |
| | "lastModified": "2024-01-02T00:00:00.000Z", |
| | "pipelineTag": "text-generation", |
| | "private": false, |
| | "tags": ["gguf", "mistral"], |
| | "createdAt": "2024-01-02T00:00:00.000Z", |
| | "updatedAt": "2024-01-02T00:00:00.000Z", |
| | "sha": "def456", |
| | "config": {}, |
| | "model_index": "test-index-2", |
| | "library_name": "transformers", |
| | "mask_token": null, |
| | "tokenizer_class": "MistralTokenizer" |
| | } |
| | ]` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | |
| | Expect(r.URL.Query().Get("sort")).To(Equal("lastModified")) |
| | Expect(r.URL.Query().Get("direction")).To(Equal("-1")) |
| | Expect(r.URL.Query().Get("limit")).To(Equal("30")) |
| | Expect(r.URL.Query().Get("search")).To(Equal("GGUF")) |
| |
|
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockResponse)) |
| | })) |
| |
|
| | |
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should successfully search for models", func() { |
| | params := hfapi.SearchParams{ |
| | Sort: "lastModified", |
| | Direction: -1, |
| | Limit: 30, |
| | Search: "GGUF", |
| | } |
| |
|
| | models, err := client.SearchModels(params) |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(models).To(HaveLen(2)) |
| |
|
| | |
| | Expect(models[0].ModelID).To(Equal("test-model-1")) |
| | Expect(models[0].Author).To(Equal("test-author")) |
| | Expect(models[0].Downloads).To(Equal(1000)) |
| | Expect(models[0].PipelineTag).To(Equal("text-generation")) |
| | Expect(models[0].Private).To(BeFalse()) |
| | Expect(models[0].Tags).To(ContainElements("gguf", "llama")) |
| |
|
| | |
| | Expect(models[1].ModelID).To(Equal("test-model-2")) |
| | Expect(models[1].Author).To(Equal("test-author-2")) |
| | Expect(models[1].Downloads).To(Equal(2000)) |
| | Expect(models[1].Tags).To(ContainElements("gguf", "mistral")) |
| | }) |
| |
|
| | It("should handle empty search results", func() { |
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte("[]")) |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| |
|
| | params := hfapi.SearchParams{ |
| | Sort: "lastModified", |
| | Direction: -1, |
| | Limit: 30, |
| | Search: "nonexistent", |
| | } |
| |
|
| | models, err := client.SearchModels(params) |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(models).To(HaveLen(0)) |
| | }) |
| |
|
| | It("should handle HTTP errors", func() { |
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | w.WriteHeader(http.StatusInternalServerError) |
| | w.Write([]byte("Internal Server Error")) |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| |
|
| | params := hfapi.SearchParams{ |
| | Sort: "lastModified", |
| | Direction: -1, |
| | Limit: 30, |
| | Search: "GGUF", |
| | } |
| |
|
| | models, err := client.SearchModels(params) |
| |
|
| | Expect(err).To(HaveOccurred()) |
| | Expect(err.Error()).To(ContainSubstring("Status code: 500")) |
| | Expect(models).To(BeNil()) |
| | }) |
| |
|
| | It("should handle malformed JSON response", func() { |
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte("invalid json")) |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| |
|
| | params := hfapi.SearchParams{ |
| | Sort: "lastModified", |
| | Direction: -1, |
| | Limit: 30, |
| | Search: "GGUF", |
| | } |
| |
|
| | models, err := client.SearchModels(params) |
| |
|
| | Expect(err).To(HaveOccurred()) |
| | Expect(err.Error()).To(ContainSubstring("failed to parse JSON response")) |
| | Expect(models).To(BeNil()) |
| | }) |
| | }) |
| |
|
| | Context("when getting latest GGUF models", func() { |
| | BeforeEach(func() { |
| | mockResponse := `[ |
| | { |
| | "modelId": "latest-gguf-model", |
| | "author": "gguf-author", |
| | "downloads": 5000, |
| | "lastModified": "2024-01-03T00:00:00.000Z", |
| | "pipelineTag": "text-generation", |
| | "private": false, |
| | "tags": ["gguf", "latest"], |
| | "createdAt": "2024-01-03T00:00:00.000Z", |
| | "updatedAt": "2024-01-03T00:00:00.000Z", |
| | "sha": "latest123", |
| | "config": {}, |
| | "model_index": "latest-index", |
| | "library_name": "transformers", |
| | "mask_token": null, |
| | "tokenizer_class": "LlamaTokenizer" |
| | } |
| | ]` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | |
| | Expect(r.URL.Query().Get("search")).To(Equal("GGUF")) |
| | Expect(r.URL.Query().Get("sort")).To(Equal("lastModified")) |
| | Expect(r.URL.Query().Get("direction")).To(Equal("-1")) |
| |
|
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockResponse)) |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should fetch latest GGUF models with correct parameters", func() { |
| | models, err := client.GetLatest("GGUF", 10) |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(models).To(HaveLen(1)) |
| | Expect(models[0].ModelID).To(Equal("latest-gguf-model")) |
| | Expect(models[0].Author).To(Equal("gguf-author")) |
| | Expect(models[0].Downloads).To(Equal(5000)) |
| | Expect(models[0].Tags).To(ContainElements("gguf", "latest")) |
| | }) |
| |
|
| | It("should use custom search term", func() { |
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | Expect(r.URL.Query().Get("search")).To(Equal("custom-search")) |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte("[]")) |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| |
|
| | models, err := client.GetLatest("custom-search", 5) |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(models).To(HaveLen(0)) |
| | }) |
| | }) |
| |
|
| | Context("when handling network errors", func() { |
| | It("should handle connection failures gracefully", func() { |
| | |
| | client.SetBaseURL("http://invalid-url-that-does-not-exist") |
| |
|
| | params := hfapi.SearchParams{ |
| | Sort: "lastModified", |
| | Direction: -1, |
| | Limit: 30, |
| | Search: "GGUF", |
| | } |
| |
|
| | models, err := client.SearchModels(params) |
| |
|
| | Expect(err).To(HaveOccurred()) |
| | Expect(err.Error()).To(ContainSubstring("failed to make request")) |
| | Expect(models).To(BeNil()) |
| | }) |
| | }) |
| |
|
| | Context("when getting file SHA on remote model", func() { |
| | It("should get file SHA successfully", func() { |
| | sha, err := client.GetFileSHA( |
| | "mudler/LocalAI-functioncall-qwen2.5-7b-v0.5-Q4_K_M-GGUF", "localai-functioncall-qwen2.5-7b-v0.5-q4_k_m.gguf") |
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(sha).To(Equal("4e7b7fe1d54b881f1ef90799219dc6cc285d29db24f559c8998d1addb35713d4")) |
| | }) |
| | }) |
| |
|
| | Context("when listing files", func() { |
| | BeforeEach(func() { |
| | mockFilesResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "model-Q4_K_M.gguf", |
| | "size": 1000000, |
| | "oid": "abc123", |
| | "lfs": { |
| | "oid": "def456789", |
| | "size": 1000000, |
| | "pointerSize": 135 |
| | } |
| | }, |
| | { |
| | "type": "file", |
| | "path": "README.md", |
| | "size": 5000, |
| | "oid": "readme123" |
| | }, |
| | { |
| | "type": "file", |
| | "path": "config.json", |
| | "size": 1000, |
| | "oid": "config123" |
| | } |
| | ]` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if strings.Contains(r.URL.Path, "/tree/main") { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockFilesResponse)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should list files successfully", func() { |
| | files, err := client.ListFiles("test/model") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(files).To(HaveLen(3)) |
| |
|
| | Expect(files[0].Path).To(Equal("model-Q4_K_M.gguf")) |
| | Expect(files[0].Size).To(Equal(int64(1000000))) |
| | Expect(files[0].LFS).ToNot(BeNil()) |
| | Expect(files[0].LFS.Oid).To(Equal("def456789")) |
| |
|
| | Expect(files[1].Path).To(Equal("README.md")) |
| | Expect(files[1].Size).To(Equal(int64(5000))) |
| | }) |
| | }) |
| |
|
| | Context("when listing files with subfolders", func() { |
| | BeforeEach(func() { |
| | |
| | mockRootResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "README.md", |
| | "size": 5000, |
| | "oid": "readme123" |
| | }, |
| | { |
| | "type": "directory", |
| | "path": "subfolder", |
| | "size": 0, |
| | "oid": "dir123" |
| | }, |
| | { |
| | "type": "file", |
| | "path": "config.json", |
| | "size": 1000, |
| | "oid": "config123" |
| | } |
| | ]` |
| |
|
| | |
| | mockSubfolderResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "subfolder/file.bin", |
| | "size": 2000000, |
| | "oid": "filebin123", |
| | "lfs": { |
| | "oid": "filebin456", |
| | "size": 2000000, |
| | "pointerSize": 135 |
| | } |
| | }, |
| | { |
| | "type": "directory", |
| | "path": "nested", |
| | "size": 0, |
| | "oid": "nesteddir123" |
| | } |
| | ]` |
| |
|
| | |
| | mockNestedResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "subfolder/nested/nested_file.gguf", |
| | "size": 5000000, |
| | "oid": "nested123", |
| | "lfs": { |
| | "oid": "nested456", |
| | "size": 5000000, |
| | "pointerSize": 135 |
| | } |
| | } |
| | ]` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | urlPath := r.URL.Path |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| |
|
| | if strings.Contains(urlPath, "/tree/main/subfolder/nested") { |
| | w.Write([]byte(mockNestedResponse)) |
| | } else if strings.Contains(urlPath, "/tree/main/subfolder") { |
| | w.Write([]byte(mockSubfolderResponse)) |
| | } else if strings.Contains(urlPath, "/tree/main") { |
| | w.Write([]byte(mockRootResponse)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should recursively list all files including those in subfolders", func() { |
| | files, err := client.ListFiles("test/model") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(files).To(HaveLen(4)) |
| |
|
| | |
| | readmeFile := findFileByPath(files, "README.md") |
| | Expect(readmeFile).ToNot(BeNil()) |
| | Expect(readmeFile.Size).To(Equal(int64(5000))) |
| | Expect(readmeFile.Oid).To(Equal("readme123")) |
| |
|
| | configFile := findFileByPath(files, "config.json") |
| | Expect(configFile).ToNot(BeNil()) |
| | Expect(configFile.Size).To(Equal(int64(1000))) |
| | Expect(configFile.Oid).To(Equal("config123")) |
| |
|
| | |
| | subfolderFile := findFileByPath(files, "subfolder/file.bin") |
| | Expect(subfolderFile).ToNot(BeNil()) |
| | Expect(subfolderFile.Size).To(Equal(int64(2000000))) |
| | Expect(subfolderFile.LFS).ToNot(BeNil()) |
| | Expect(subfolderFile.LFS.Oid).To(Equal("filebin456")) |
| |
|
| | |
| | nestedFile := findFileByPath(files, "subfolder/nested/nested_file.gguf") |
| | Expect(nestedFile).ToNot(BeNil()) |
| | Expect(nestedFile.Size).To(Equal(int64(5000000))) |
| | Expect(nestedFile.LFS).ToNot(BeNil()) |
| | Expect(nestedFile.LFS.Oid).To(Equal("nested456")) |
| | }) |
| |
|
| | It("should handle files with correct relative paths", func() { |
| | files, err := client.ListFiles("test/model") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| |
|
| | |
| | paths := make([]string, len(files)) |
| | for i, file := range files { |
| | paths[i] = file.Path |
| | } |
| |
|
| | Expect(paths).To(ContainElements( |
| | "README.md", |
| | "config.json", |
| | "subfolder/file.bin", |
| | "subfolder/nested/nested_file.gguf", |
| | )) |
| | }) |
| | }) |
| |
|
| | Context("when getting file SHA", func() { |
| | BeforeEach(func() { |
| | mockFilesResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "model-Q4_K_M.gguf", |
| | "size": 1000000, |
| | "oid": "abc123", |
| | "lfs": { |
| | "oid": "def456789", |
| | "size": 1000000, |
| | "pointerSize": 135 |
| | } |
| | } |
| | ]` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if strings.Contains(r.URL.Path, "/tree/main") { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockFilesResponse)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should get file SHA successfully", func() { |
| | sha, err := client.GetFileSHA("test/model", "model-Q4_K_M.gguf") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(sha).To(Equal("def456789")) |
| | }) |
| |
|
| | It("should handle missing SHA gracefully", func() { |
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if strings.Contains(r.URL.Path, "/tree/main") { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(`[ |
| | { |
| | "type": "file", |
| | "path": "file.txt", |
| | "size": 100, |
| | "oid": "file123" |
| | } |
| | ]`)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| |
|
| | sha, err := client.GetFileSHA("test/model", "file.txt") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | |
| | Expect(sha).To(Equal("file123")) |
| | }) |
| | }) |
| |
|
| | Context("when getting model details", func() { |
| | BeforeEach(func() { |
| | mockFilesResponse := `[ |
| | { |
| | "type": "file", |
| | "path": "model-Q4_K_M.gguf", |
| | "size": 1000000, |
| | "oid": "abc123", |
| | "lfs": { |
| | "oid": "sha256:def456", |
| | "size": 1000000, |
| | "pointer": "version https://git-lfs.github.com/spec/v1", |
| | "sha256": "def456789" |
| | } |
| | }, |
| | { |
| | "type": "file", |
| | "path": "README.md", |
| | "size": 5000, |
| | "oid": "readme123" |
| | } |
| | ]` |
| |
|
| | mockFileInfoResponse := `{ |
| | "path": "model-Q4_K_M.gguf", |
| | "size": 1000000, |
| | "oid": "abc123", |
| | "lfs": { |
| | "oid": "sha256:def456", |
| | "size": 1000000, |
| | "pointer": "version https://git-lfs.github.com/spec/v1", |
| | "sha256": "def456789" |
| | } |
| | }` |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if strings.Contains(r.URL.Path, "/tree/main") { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockFilesResponse)) |
| | } else if strings.Contains(r.URL.Path, "/paths-info") { |
| | w.Header().Set("Content-Type", "application/json") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockFileInfoResponse)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should get model details successfully", func() { |
| | details, err := client.GetModelDetails("test/model") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(details.ModelID).To(Equal("test/model")) |
| | Expect(details.Author).To(Equal("test")) |
| | Expect(details.Files).To(HaveLen(2)) |
| |
|
| | Expect(details.ReadmeFile).ToNot(BeNil()) |
| | Expect(details.ReadmeFile.Path).To(Equal("README.md")) |
| | Expect(details.ReadmeFile.IsReadme).To(BeTrue()) |
| |
|
| | |
| | baseURL := strings.TrimSuffix(server.URL, "/api/models") |
| | for _, file := range details.Files { |
| | expectedURL := fmt.Sprintf("%s/test/model/resolve/main/%s", baseURL, file.Path) |
| | Expect(file.URL).To(Equal(expectedURL)) |
| | } |
| | }) |
| | }) |
| |
|
| | Context("when getting README content", func() { |
| | BeforeEach(func() { |
| | mockReadmeContent := "# Test Model\n\nThis is a test model for demonstration purposes." |
| |
|
| | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | if strings.Contains(r.URL.Path, "/raw/main/") { |
| | w.Header().Set("Content-Type", "text/plain") |
| | w.WriteHeader(http.StatusOK) |
| | w.Write([]byte(mockReadmeContent)) |
| | } else { |
| | w.WriteHeader(http.StatusNotFound) |
| | } |
| | })) |
| |
|
| | client.SetBaseURL(server.URL) |
| | }) |
| |
|
| | It("should get README content successfully", func() { |
| | content, err := client.GetReadmeContent("test/model", "README.md") |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(content).To(Equal("# Test Model\n\nThis is a test model for demonstration purposes.")) |
| | }) |
| | }) |
| |
|
| | Context("when filtering files", func() { |
| | It("should filter files by quantization", func() { |
| | files := []hfapi.ModelFile{ |
| | {Path: "model-Q4_K_M.gguf"}, |
| | {Path: "model-Q3_K_M.gguf"}, |
| | {Path: "README.md", IsReadme: true}, |
| | } |
| |
|
| | filtered := hfapi.FilterFilesByQuantization(files, "Q4_K_M") |
| |
|
| | Expect(filtered).To(HaveLen(1)) |
| | Expect(filtered[0].Path).To(Equal("model-Q4_K_M.gguf")) |
| | }) |
| |
|
| | It("should find preferred model file", func() { |
| | files := []hfapi.ModelFile{ |
| | {Path: "model-Q3_K_M.gguf"}, |
| | {Path: "model-Q4_K_M.gguf"}, |
| | {Path: "README.md", IsReadme: true}, |
| | } |
| |
|
| | preferences := []string{"Q4_K_M", "Q3_K_M"} |
| | preferred := hfapi.FindPreferredModelFile(files, preferences) |
| |
|
| | Expect(preferred).ToNot(BeNil()) |
| | Expect(preferred.Path).To(Equal("model-Q4_K_M.gguf")) |
| | Expect(preferred.IsReadme).To(BeFalse()) |
| | }) |
| |
|
| | It("should return nil if no preferred file found", func() { |
| | files := []hfapi.ModelFile{ |
| | {Path: "model-Q2_K.gguf"}, |
| | {Path: "README.md", IsReadme: true}, |
| | } |
| |
|
| | preferences := []string{"Q4_K_M", "Q3_K_M"} |
| | preferred := hfapi.FindPreferredModelFile(files, preferences) |
| |
|
| | Expect(preferred).To(BeNil()) |
| | }) |
| | }) |
| |
|
| | Context("integration test with real HuggingFace API", func() { |
| | It("should recursively list all files including subfolders from real repository", func() { |
| | |
| | |
| | realClient := hfapi.NewClient() |
| | repoID := "bartowski/Qwen_Qwen3-Next-80B-A3B-Instruct-GGUF" |
| |
|
| | files, err := realClient.ListFiles(repoID) |
| |
|
| | Expect(err).ToNot(HaveOccurred()) |
| | Expect(files).ToNot(BeEmpty(), "should return at least some files") |
| |
|
| | |
| | |
| | |
| | |
| | |
| | hasSubfolderFiles := false |
| | rootLevelFiles := 0 |
| | subfolderFiles := 0 |
| |
|
| | for _, file := range files { |
| | if strings.Contains(file.Path, "/") { |
| | hasSubfolderFiles = true |
| | subfolderFiles++ |
| | |
| | Expect(file.Path).ToNot(HavePrefix("/"), "paths should be relative, not absolute") |
| | Expect(file.Path).ToNot(HaveSuffix("/"), "file paths should not end with /") |
| | } else { |
| | rootLevelFiles++ |
| | } |
| | } |
| |
|
| | Expect(hasSubfolderFiles).To(BeTrue(), "should find files in subfolders") |
| | Expect(rootLevelFiles).To(BeNumerically(">", 0), "should find files at root level") |
| | Expect(subfolderFiles).To(BeNumerically(">", 0), "should find files in subfolders") |
| | |
| | |
| | readmeFile := findFileByPath(files, "README.md") |
| | Expect(readmeFile).ToNot(BeNil(), "README.md should exist at root level") |
| |
|
| | |
| | |
| | foundSubfolderFile := false |
| | for _, file := range files { |
| | if strings.Contains(file.Path, "/") && strings.HasSuffix(file.Path, ".gguf") { |
| | foundSubfolderFile = true |
| | |
| | parts := strings.Split(file.Path, "/") |
| | Expect(len(parts)).To(BeNumerically(">=", 2), "subfolder files should have at least subfolder/file.gguf format") |
| | |
| | Expect(parts[len(parts)-1]).To(HaveSuffix(".gguf"), "file in subfolder should be a .gguf file") |
| | Expect(parts[len(parts)-1]).ToNot(BeEmpty(), "filename should not be empty") |
| | break |
| | } |
| | } |
| | Expect(foundSubfolderFile).To(BeTrue(), "should find at least one .gguf file in a subfolder") |
| |
|
| | |
| | for _, file := range files { |
| | Expect(file.Path).ToNot(BeEmpty(), "file path should not be empty") |
| | Expect(file.Type).To(Equal("file"), "all returned items should be files, not directories") |
| | |
| | if file.LFS == nil { |
| | Expect(file.Oid).ToNot(BeEmpty(), "file should have an OID if no LFS") |
| | } |
| | } |
| | }) |
| | }) |
| | }) |
| |
|
| | |
| | func findFileByPath(files []hfapi.FileInfo, path string) *hfapi.FileInfo { |
| | for i := range files { |
| | if files[i].Path == path { |
| | return &files[i] |
| | } |
| | } |
| | return nil |
| | } |
| |
|