Merge pull request #6172 from menloresearch/fix/model-id-special-char
fix: handle modelId special char
This commit is contained in:
commit
a66d83c598
2
.github/workflows/jan-docs.yml
vendored
2
.github/workflows/jan-docs.yml
vendored
@ -76,7 +76,7 @@ jobs:
|
|||||||
Preview URL: ${{ steps.deployCloudflarePages.outputs.url }}
|
Preview URL: ${{ steps.deployCloudflarePages.outputs.url }}
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages Production
|
- name: Publish to Cloudflare Pages Production
|
||||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
if: (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/release/'))
|
||||||
uses: cloudflare/pages-action@v1
|
uses: cloudflare/pages-action@v1
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
25
.github/workflows/jan-tauri-build.yaml
vendored
25
.github/workflows/jan-tauri-build.yaml
vendored
@ -32,6 +32,7 @@ jobs:
|
|||||||
name: "${{ env.VERSION }}"
|
name: "${{ env.VERSION }}"
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
uses: ./.github/workflows/template-tauri-build-macos.yml
|
uses: ./.github/workflows/template-tauri-build-macos.yml
|
||||||
@ -119,27 +120,3 @@ jobs:
|
|||||||
asset_path: ./latest.json
|
asset_path: ./latest.json
|
||||||
asset_name: latest.json
|
asset_name: latest.json
|
||||||
asset_content_type: text/json
|
asset_content_type: text/json
|
||||||
|
|
||||||
update_release_draft:
|
|
||||||
needs: [build-macos, build-windows-x64, build-linux-x64]
|
|
||||||
permissions:
|
|
||||||
# write permission is required to create a github release
|
|
||||||
contents: write
|
|
||||||
# write permission is required for autolabeler
|
|
||||||
# otherwise, read permission is required at least
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# (Optional) GitHub Enterprise requires GHE_HOST variable set
|
|
||||||
#- name: Set GHE_HOST
|
|
||||||
# run: |
|
|
||||||
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
|
||||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
|
||||||
# with:
|
|
||||||
# config-name: my-config.yml
|
|
||||||
# disable-autolabeler: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@ -1172,7 +1172,7 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
const [version, backend] = cfg.version_backend.split('/')
|
const [version, backend] = cfg.version_backend.split('/')
|
||||||
if (!version || !backend) {
|
if (!version || !backend) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid version/backend format: ${cfg.version_backend}. Expected format: <version>/<backend>`
|
"Initial setup for the backend failed due to a network issue. Please restart the app!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,16 @@ export default function LoadModelErrorDialog() {
|
|||||||
return copyText
|
return copyText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'object') {
|
||||||
|
const errorObj = error as {
|
||||||
|
code?: string
|
||||||
|
message: string
|
||||||
|
details?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorObj.message
|
||||||
|
}
|
||||||
|
|
||||||
return JSON.stringify(error)
|
return JSON.stringify(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,11 @@ vi.mock('@/services/models', () => ({
|
|||||||
fetchModelCatalog: vi.fn(),
|
fetchModelCatalog: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock the sanitizeModelId function
|
||||||
|
vi.mock('@/lib/utils', () => ({
|
||||||
|
sanitizeModelId: vi.fn((id: string) => id),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('useModelSources', () => {
|
describe('useModelSources', () => {
|
||||||
let mockFetchModelCatalog: any
|
let mockFetchModelCatalog: any
|
||||||
|
|
||||||
@ -56,15 +61,19 @@ describe('useModelSources', () => {
|
|||||||
const mockSources: CatalogModel[] = [
|
const mockSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'model-1',
|
model_name: 'model-1',
|
||||||
provider: 'provider-1',
|
|
||||||
description: 'First model',
|
description: 'First model',
|
||||||
version: '1.0.0',
|
developer: 'provider-1',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model_name: 'model-2',
|
model_name: 'model-2',
|
||||||
provider: 'provider-2',
|
|
||||||
description: 'Second model',
|
description: 'Second model',
|
||||||
version: '2.0.0',
|
developer: 'provider-2',
|
||||||
|
downloads: 200,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -101,18 +110,22 @@ describe('useModelSources', () => {
|
|||||||
const existingSources: CatalogModel[] = [
|
const existingSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'existing-model',
|
model_name: 'existing-model',
|
||||||
provider: 'existing-provider',
|
|
||||||
description: 'Existing model',
|
description: 'Existing model',
|
||||||
version: '1.0.0',
|
developer: 'existing-provider',
|
||||||
|
downloads: 50,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'existing-model-q4', path: '/path/existing', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const newSources: CatalogModel[] = [
|
const newSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'new-model',
|
model_name: 'new-model',
|
||||||
provider: 'new-provider',
|
|
||||||
description: 'New model',
|
description: 'New model',
|
||||||
version: '2.0.0',
|
developer: 'new-provider',
|
||||||
|
downloads: 150,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'new-model-q4', path: '/path/new', file_size: '2GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -138,24 +151,30 @@ describe('useModelSources', () => {
|
|||||||
const existingSources: CatalogModel[] = [
|
const existingSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'duplicate-model',
|
model_name: 'duplicate-model',
|
||||||
provider: 'old-provider',
|
|
||||||
description: 'Old version',
|
description: 'Old version',
|
||||||
version: '1.0.0',
|
developer: 'old-provider',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'duplicate-model-q4', path: '/path/old', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model_name: 'unique-model',
|
model_name: 'unique-model',
|
||||||
provider: 'provider',
|
|
||||||
description: 'Unique model',
|
description: 'Unique model',
|
||||||
version: '1.0.0',
|
developer: 'provider',
|
||||||
|
downloads: 75,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'unique-model-q4', path: '/path/unique', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const newSources: CatalogModel[] = [
|
const newSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'duplicate-model',
|
model_name: 'duplicate-model',
|
||||||
provider: 'new-provider',
|
|
||||||
description: 'New version',
|
description: 'New version',
|
||||||
version: '2.0.0',
|
developer: 'new-provider',
|
||||||
|
downloads: 200,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'duplicate-model-q4-new', path: '/path/new', file_size: '2GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -207,9 +226,11 @@ describe('useModelSources', () => {
|
|||||||
const mockSources: CatalogModel[] = [
|
const mockSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'model-1',
|
model_name: 'model-1',
|
||||||
provider: 'provider-1',
|
|
||||||
description: 'Model 1',
|
description: 'Model 1',
|
||||||
version: '1.0.0',
|
developer: 'provider-1',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -238,9 +259,11 @@ describe('useModelSources', () => {
|
|||||||
const mockSources: CatalogModel[] = [
|
const mockSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'shared-model',
|
model_name: 'shared-model',
|
||||||
provider: 'shared-provider',
|
|
||||||
description: 'Shared model',
|
description: 'Shared model',
|
||||||
version: '1.0.0',
|
developer: 'shared-provider',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'shared-model-q4', path: '/path/shared', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -288,18 +311,22 @@ describe('useModelSources', () => {
|
|||||||
const sources1: CatalogModel[] = [
|
const sources1: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'model-1',
|
model_name: 'model-1',
|
||||||
provider: 'provider-1',
|
|
||||||
description: 'First batch',
|
description: 'First batch',
|
||||||
version: '1.0.0',
|
developer: 'provider-1',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const sources2: CatalogModel[] = [
|
const sources2: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'model-2',
|
model_name: 'model-2',
|
||||||
provider: 'provider-2',
|
|
||||||
description: 'Second batch',
|
description: 'Second batch',
|
||||||
version: '2.0.0',
|
developer: 'provider-2',
|
||||||
|
downloads: 200,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -338,9 +365,11 @@ describe('useModelSources', () => {
|
|||||||
const mockSources: CatalogModel[] = [
|
const mockSources: CatalogModel[] = [
|
||||||
{
|
{
|
||||||
model_name: 'recovery-model',
|
model_name: 'recovery-model',
|
||||||
provider: 'recovery-provider',
|
|
||||||
description: 'Recovery model',
|
description: 'Recovery model',
|
||||||
version: '1.0.0',
|
developer: 'recovery-provider',
|
||||||
|
downloads: 100,
|
||||||
|
num_quants: 1,
|
||||||
|
quants: [{ model_id: 'recovery-model-q4', path: '/path/recovery', file_size: '1GB' }],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -227,34 +227,23 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration for cont_batching description update (version 0 -> 1)
|
|
||||||
if (version === 0 && state?.providers) {
|
if (version === 0 && state?.providers) {
|
||||||
state.providers = state.providers.map((provider) => {
|
state.providers.forEach((provider) => {
|
||||||
|
// Update cont_batching description for llamacpp provider
|
||||||
if (provider.provider === 'llamacpp' && provider.settings) {
|
if (provider.provider === 'llamacpp' && provider.settings) {
|
||||||
provider.settings = provider.settings.map((setting) => {
|
const contBatchingSetting = provider.settings.find(
|
||||||
if (setting.key === 'cont_batching') {
|
(s) => s.key === 'cont_batching'
|
||||||
return {
|
)
|
||||||
...setting,
|
if (contBatchingSetting) {
|
||||||
description:
|
contBatchingSetting.description =
|
||||||
'Enable continuous batching (a.k.a dynamic batching) for concurrent requests.',
|
'Enable continuous batching (a.k.a dynamic batching) for concurrent requests.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setting
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return provider
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration for chatTemplate key to chat_template (version 1 -> 2)
|
// Migrate model settings
|
||||||
if (version === 1 && state?.providers) {
|
|
||||||
state.providers.forEach((provider) => {
|
|
||||||
if (provider.models) {
|
if (provider.models) {
|
||||||
provider.models.forEach((model) => {
|
provider.models.forEach((model) => {
|
||||||
// Initialize settings if it doesn't exist
|
if (!model.settings) model.settings = {}
|
||||||
if (!model.settings) {
|
|
||||||
model.settings = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate chatTemplate key to chat_template
|
// Migrate chatTemplate key to chat_template
|
||||||
if (model.settings.chatTemplate) {
|
if (model.settings.chatTemplate) {
|
||||||
@ -262,7 +251,7 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
delete model.settings.chatTemplate
|
delete model.settings.chatTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add missing chat_template setting if it doesn't exist
|
// Add missing settings with defaults
|
||||||
if (!model.settings.chat_template) {
|
if (!model.settings.chat_template) {
|
||||||
model.settings.chat_template = {
|
model.settings.chat_template = {
|
||||||
...modelSettings.chatTemplate,
|
...modelSettings.chatTemplate,
|
||||||
@ -271,22 +260,7 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration for override_tensor_buffer_type key (version 2 -> 3)
|
|
||||||
if (version === 2 && state?.providers) {
|
|
||||||
state.providers.forEach((provider) => {
|
|
||||||
if (provider.models) {
|
|
||||||
provider.models.forEach((model) => {
|
|
||||||
// Initialize settings if it doesn't exist
|
|
||||||
if (!model.settings) {
|
|
||||||
model.settings = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing override_tensor_buffer_type setting if it doesn't exist
|
|
||||||
if (!model.settings.override_tensor_buffer_t) {
|
if (!model.settings.override_tensor_buffer_t) {
|
||||||
model.settings.override_tensor_buffer_t = {
|
model.settings.override_tensor_buffer_t = {
|
||||||
...modelSettings.override_tensor_buffer_t,
|
...modelSettings.override_tensor_buffer_t,
|
||||||
@ -303,7 +277,7 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
version: 3,
|
version: 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { create } from 'zustand'
|
|||||||
import { localStorageKey } from '@/constants/localStorage'
|
import { localStorageKey } from '@/constants/localStorage'
|
||||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||||
import { fetchModelCatalog, CatalogModel } from '@/services/models'
|
import { fetchModelCatalog, CatalogModel } from '@/services/models'
|
||||||
|
import { sanitizeModelId } from '@/lib/utils'
|
||||||
|
|
||||||
// Zustand store for model sources
|
// Zustand store for model sources
|
||||||
type ModelSourcesState = {
|
type ModelSourcesState = {
|
||||||
@ -20,7 +21,15 @@ export const useModelSources = create<ModelSourcesState>()(
|
|||||||
fetchSources: async () => {
|
fetchSources: async () => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const newSources = await fetchModelCatalog()
|
const newSources = await fetchModelCatalog().then((catalogs) =>
|
||||||
|
catalogs.map((catalog) => ({
|
||||||
|
...catalog,
|
||||||
|
quants: catalog.quants.map((quant) => ({
|
||||||
|
...quant,
|
||||||
|
model_id: sanitizeModelId(quant.model_id),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
set({
|
set({
|
||||||
sources: newSources.length ? newSources : get().sources,
|
sources: newSources.length ? newSources : get().sources,
|
||||||
|
|||||||
@ -155,3 +155,7 @@ export function formatDuration(startTime: number, endTime?: number): string {
|
|||||||
return `${durationMs}ms`
|
return `${durationMs}ms`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeModelId(modelId: string): string {
|
||||||
|
return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(/\./g, "_")
|
||||||
|
}
|
||||||
|
|||||||
@ -132,7 +132,9 @@ function Hub() {
|
|||||||
// Apply search filter
|
// Apply search filter
|
||||||
if (debouncedSearchValue.length) {
|
if (debouncedSearchValue.length) {
|
||||||
const fuse = new Fuse(filtered, searchOptions)
|
const fuse = new Fuse(filtered, searchOptions)
|
||||||
filtered = fuse.search(debouncedSearchValue).map((result) => result.item)
|
// Remove domain from search value (e.g., "huggingface.co/author/model" -> "author/model")
|
||||||
|
const cleanedSearchValue = debouncedSearchValue.replace(/^https?:\/\/[^/]+\//, '')
|
||||||
|
filtered = fuse.search(cleanedSearchValue).map((result) => result.item)
|
||||||
}
|
}
|
||||||
// Apply downloaded filter
|
// Apply downloaded filter
|
||||||
if (showOnlyDownloaded) {
|
if (showOnlyDownloaded) {
|
||||||
@ -194,7 +196,11 @@ function Hub() {
|
|||||||
if (repoInfo) {
|
if (repoInfo) {
|
||||||
const catalogModel = convertHfRepoToCatalogModel(repoInfo)
|
const catalogModel = convertHfRepoToCatalogModel(repoInfo)
|
||||||
if (
|
if (
|
||||||
!sources.some((s) => s.model_name === catalogModel.model_name)
|
!sources.some(
|
||||||
|
(s) =>
|
||||||
|
catalogModel.model_name.trim().split('/').pop() ===
|
||||||
|
s.model_name.trim()
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
setHuggingFaceRepo(catalogModel)
|
setHuggingFaceRepo(catalogModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
||||||
import { Route as GeneralRoute } from '../general'
|
import { Route as GeneralRoute } from '../general'
|
||||||
|
|
||||||
// Mock all the dependencies
|
// Mock all the dependencies
|
||||||
@ -68,9 +68,12 @@ vi.mock('@/hooks/useGeneralSetting', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Create a controllable mock
|
||||||
|
const mockCheckForUpdate = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/hooks/useAppUpdater', () => ({
|
vi.mock('@/hooks/useAppUpdater', () => ({
|
||||||
useAppUpdater: () => ({
|
useAppUpdater: () => ({
|
||||||
checkForUpdate: vi.fn(),
|
checkForUpdate: mockCheckForUpdate,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -184,12 +187,17 @@ vi.mock('@tauri-apps/plugin-opener', () => ({
|
|||||||
revealItemInDir: vi.fn(),
|
revealItemInDir: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@tauri-apps/api/webviewWindow', () => ({
|
vi.mock('@tauri-apps/api/webviewWindow', () => {
|
||||||
WebviewWindow: vi.fn().mockImplementation((label: string, options: any) => ({
|
const MockWebviewWindow = vi.fn().mockImplementation((label: string, options: any) => ({
|
||||||
once: vi.fn(),
|
once: vi.fn(),
|
||||||
setFocus: vi.fn(),
|
setFocus: vi.fn(),
|
||||||
})),
|
|
||||||
}))
|
}))
|
||||||
|
MockWebviewWindow.getByLabel = vi.fn().mockReturnValue(null)
|
||||||
|
|
||||||
|
return {
|
||||||
|
WebviewWindow: MockWebviewWindow,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@tauri-apps/api/event', () => ({
|
vi.mock('@tauri-apps/api/event', () => ({
|
||||||
emit: vi.fn(),
|
emit: vi.fn(),
|
||||||
@ -244,6 +252,7 @@ global.window = {
|
|||||||
core: {
|
core: {
|
||||||
api: {
|
api: {
|
||||||
relaunch: vi.fn(),
|
relaunch: vi.fn(),
|
||||||
|
getConnectedServers: vi.fn().mockResolvedValue([]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -258,20 +267,26 @@ Object.assign(navigator, {
|
|||||||
describe('General Settings Route', () => {
|
describe('General Settings Route', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
// Reset the mock to return a promise that resolves immediately by default
|
||||||
|
mockCheckForUpdate.mockResolvedValue(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render the general settings page', () => {
|
it('should render the general settings page', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
|
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
|
||||||
expect(screen.getByText('common:settings')).toBeInTheDocument()
|
expect(screen.getByText('common:settings')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render app version', () => {
|
it('should render app version', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
expect(screen.getByText('v1.0.0')).toBeInTheDocument()
|
expect(screen.getByText('v1.0.0')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@ -284,64 +299,82 @@ describe('General Settings Route', () => {
|
|||||||
// expect(screen.getByTestId('language-switcher')).toBeInTheDocument()
|
// expect(screen.getByTestId('language-switcher')).toBeInTheDocument()
|
||||||
// })
|
// })
|
||||||
|
|
||||||
it('should render switches for experimental features and spell check', () => {
|
it('should render switches for experimental features and spell check', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const switches = screen.getAllByTestId('switch')
|
const switches = screen.getAllByTestId('switch')
|
||||||
expect(switches.length).toBeGreaterThanOrEqual(2)
|
expect(switches.length).toBeGreaterThanOrEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render huggingface token input', () => {
|
it('should render huggingface token input', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const input = screen.getByTestId('input')
|
const input = screen.getByTestId('input')
|
||||||
expect(input).toBeInTheDocument()
|
expect(input).toBeInTheDocument()
|
||||||
expect(input).toHaveValue('test-token')
|
expect(input).toHaveValue('test-token')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle spell check toggle', () => {
|
it('should handle spell check toggle', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const switches = screen.getAllByTestId('switch')
|
const switches = screen.getAllByTestId('switch')
|
||||||
expect(switches.length).toBeGreaterThan(0)
|
expect(switches.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Test that switches are interactive
|
// Test that switches are interactive
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(switches[0])
|
fireEvent.click(switches[0])
|
||||||
|
})
|
||||||
expect(switches[0]).toBeInTheDocument()
|
expect(switches[0]).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle experimental features toggle', () => {
|
it('should handle experimental features toggle', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const switches = screen.getAllByTestId('switch')
|
const switches = screen.getAllByTestId('switch')
|
||||||
expect(switches.length).toBeGreaterThan(0)
|
expect(switches.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Test that switches are interactive
|
// Test that switches are interactive
|
||||||
if (switches.length > 1) {
|
if (switches.length > 1) {
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(switches[1])
|
fireEvent.click(switches[1])
|
||||||
|
})
|
||||||
expect(switches[1]).toBeInTheDocument()
|
expect(switches[1]).toBeInTheDocument()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle huggingface token change', () => {
|
it('should handle huggingface token change', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const input = screen.getByTestId('input')
|
const input = screen.getByTestId('input')
|
||||||
expect(input).toBeInTheDocument()
|
expect(input).toBeInTheDocument()
|
||||||
|
|
||||||
// Test that input is interactive
|
// Test that input is interactive
|
||||||
|
await act(async () => {
|
||||||
fireEvent.change(input, { target: { value: 'new-token' } })
|
fireEvent.change(input, { target: { value: 'new-token' } })
|
||||||
|
})
|
||||||
expect(input).toBeInTheDocument()
|
expect(input).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle check for updates', async () => {
|
it('should handle check for updates', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const buttons = screen.getAllByTestId('button')
|
const buttons = screen.getAllByTestId('button')
|
||||||
const checkUpdateButton = buttons.find((button) =>
|
const checkUpdateButton = buttons.find((button) =>
|
||||||
@ -350,7 +383,9 @@ describe('General Settings Route', () => {
|
|||||||
|
|
||||||
if (checkUpdateButton) {
|
if (checkUpdateButton) {
|
||||||
expect(checkUpdateButton).toBeInTheDocument()
|
expect(checkUpdateButton).toBeInTheDocument()
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(checkUpdateButton)
|
fireEvent.click(checkUpdateButton)
|
||||||
|
})
|
||||||
// Test that button is interactive
|
// Test that button is interactive
|
||||||
expect(checkUpdateButton).toBeInTheDocument()
|
expect(checkUpdateButton).toBeInTheDocument()
|
||||||
}
|
}
|
||||||
@ -358,7 +393,9 @@ describe('General Settings Route', () => {
|
|||||||
|
|
||||||
it('should handle data folder display', async () => {
|
it('should handle data folder display', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
// Test that component renders without errors
|
// Test that component renders without errors
|
||||||
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
||||||
@ -367,25 +404,31 @@ describe('General Settings Route', () => {
|
|||||||
|
|
||||||
it('should handle copy to clipboard', async () => {
|
it('should handle copy to clipboard', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
// Test that component renders without errors
|
// Test that component renders without errors
|
||||||
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
expect(screen.getByTestId('header-page')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
|
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle factory reset dialog', () => {
|
it('should handle factory reset dialog', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('dialog')).toBeInTheDocument()
|
expect(screen.getByTestId('dialog')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('dialog-trigger')).toBeInTheDocument()
|
expect(screen.getByTestId('dialog-trigger')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
|
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render external links', () => {
|
it('should render external links', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
// Check for external links
|
// Check for external links
|
||||||
const links = screen.getAllByRole('link')
|
const links = screen.getAllByRole('link')
|
||||||
@ -394,7 +437,9 @@ describe('General Settings Route', () => {
|
|||||||
|
|
||||||
it('should handle logs window opening', async () => {
|
it('should handle logs window opening', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const buttons = screen.getAllByTestId('button')
|
const buttons = screen.getAllByTestId('button')
|
||||||
const openLogsButton = buttons.find((button) =>
|
const openLogsButton = buttons.find((button) =>
|
||||||
@ -404,14 +449,18 @@ describe('General Settings Route', () => {
|
|||||||
if (openLogsButton) {
|
if (openLogsButton) {
|
||||||
expect(openLogsButton).toBeInTheDocument()
|
expect(openLogsButton).toBeInTheDocument()
|
||||||
// Test that button is interactive
|
// Test that button is interactive
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(openLogsButton)
|
fireEvent.click(openLogsButton)
|
||||||
|
})
|
||||||
expect(openLogsButton).toBeInTheDocument()
|
expect(openLogsButton).toBeInTheDocument()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle reveal logs folder', async () => {
|
it('should handle reveal logs folder', async () => {
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const buttons = screen.getAllByTestId('button')
|
const buttons = screen.getAllByTestId('button')
|
||||||
const revealLogsButton = buttons.find((button) =>
|
const revealLogsButton = buttons.find((button) =>
|
||||||
@ -421,26 +470,39 @@ describe('General Settings Route', () => {
|
|||||||
if (revealLogsButton) {
|
if (revealLogsButton) {
|
||||||
expect(revealLogsButton).toBeInTheDocument()
|
expect(revealLogsButton).toBeInTheDocument()
|
||||||
// Test that button is interactive
|
// Test that button is interactive
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(revealLogsButton)
|
fireEvent.click(revealLogsButton)
|
||||||
|
})
|
||||||
expect(revealLogsButton).toBeInTheDocument()
|
expect(revealLogsButton).toBeInTheDocument()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show correct file explorer text for Windows', () => {
|
it('should show correct file explorer text for Windows', async () => {
|
||||||
global.IS_WINDOWS = true
|
global.IS_WINDOWS = true
|
||||||
global.IS_MACOS = false
|
global.IS_MACOS = false
|
||||||
|
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('settings:general.showInFileExplorer')
|
screen.getByText('settings:general.showInFileExplorer')
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should disable check for updates button when checking', () => {
|
it('should disable check for updates button when checking', async () => {
|
||||||
|
// Create a promise that we can control
|
||||||
|
let resolveUpdate: (value: any) => void
|
||||||
|
const updatePromise = new Promise((resolve) => {
|
||||||
|
resolveUpdate = resolve
|
||||||
|
})
|
||||||
|
mockCheckForUpdate.mockReturnValue(updatePromise)
|
||||||
|
|
||||||
const Component = GeneralRoute.component as React.ComponentType
|
const Component = GeneralRoute.component as React.ComponentType
|
||||||
|
await act(async () => {
|
||||||
render(<Component />)
|
render(<Component />)
|
||||||
|
})
|
||||||
|
|
||||||
const buttons = screen.getAllByTestId('button')
|
const buttons = screen.getAllByTestId('button')
|
||||||
const checkUpdateButton = buttons.find((button) =>
|
const checkUpdateButton = buttons.find((button) =>
|
||||||
@ -448,8 +510,22 @@ describe('General Settings Route', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (checkUpdateButton) {
|
if (checkUpdateButton) {
|
||||||
|
// Click the button but don't await it yet
|
||||||
|
act(() => {
|
||||||
fireEvent.click(checkUpdateButton)
|
fireEvent.click(checkUpdateButton)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now the button should be disabled while checking
|
||||||
expect(checkUpdateButton).toBeDisabled()
|
expect(checkUpdateButton).toBeDisabled()
|
||||||
|
|
||||||
|
// Resolve the promise to finish the update check
|
||||||
|
await act(async () => {
|
||||||
|
resolveUpdate!(null)
|
||||||
|
await updatePromise
|
||||||
|
})
|
||||||
|
|
||||||
|
// Button should be enabled again
|
||||||
|
expect(checkUpdateButton).not.toBeDisabled()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { sanitizeModelId } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
AIEngine,
|
AIEngine,
|
||||||
EngineManager,
|
EngineManager,
|
||||||
@ -172,7 +173,7 @@ export const convertHfRepoToCatalogModel = (
|
|||||||
const modelId = file.rfilename.replace(/\.gguf$/i, '')
|
const modelId = file.rfilename.replace(/\.gguf$/i, '')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model_id: modelId,
|
model_id: sanitizeModelId(modelId),
|
||||||
path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`,
|
path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`,
|
||||||
file_size: formatFileSize(file.size),
|
file_size: formatFileSize(file.size),
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user