chore: improve models and threads caching (#3744)
* chore: managing and maintaining models and threads in the cache * test: add tests for hooks
This commit is contained in:
parent
87a8bc7359
commit
87e1754e3a
@ -16,14 +16,12 @@ import { mainViewStateAtom, showLeftPanelAtom } from '@/helpers/atoms/App.atom'
|
||||
import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
|
||||
import { isDownloadALocalModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
import {
|
||||
reduceTransparentAtom,
|
||||
selectedSettingAtom,
|
||||
} from '@/helpers/atoms/Setting.atom'
|
||||
import {
|
||||
isDownloadALocalModelAtom,
|
||||
threadsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
import { threadsAtom } from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
export default function RibbonPanel() {
|
||||
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
||||
|
||||
@ -513,7 +513,7 @@ const ModelDropdown = ({
|
||||
const isDownloading = downloadingModels.some(
|
||||
(md) => md.id === model.id
|
||||
)
|
||||
const isdDownloaded = downloadedModels.some(
|
||||
const isDownloaded = downloadedModels.some(
|
||||
(c) => c.id === model.id
|
||||
)
|
||||
return (
|
||||
@ -528,7 +528,7 @@ const ModelDropdown = ({
|
||||
onClick={() => {
|
||||
if (!apiKey && !isLocalEngine(model.engine))
|
||||
return null
|
||||
if (isdDownloaded) {
|
||||
if (isDownloaded) {
|
||||
onClickModelItem(model.id)
|
||||
}
|
||||
}}
|
||||
@ -537,7 +537,7 @@ const ModelDropdown = ({
|
||||
<p
|
||||
className={twMerge(
|
||||
'line-clamp-1',
|
||||
!isdDownloaded &&
|
||||
!isDownloaded &&
|
||||
'text-[hsla(var(--text-secondary))]'
|
||||
)}
|
||||
title={model.name}
|
||||
@ -547,12 +547,12 @@ const ModelDropdown = ({
|
||||
<ModelLabel metadata={model.metadata} compact />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
||||
{!isdDownloaded && (
|
||||
{!isDownloaded && (
|
||||
<span className="font-medium">
|
||||
{toGibibytes(model.metadata.size)}
|
||||
</span>
|
||||
)}
|
||||
{!isDownloading && !isdDownloaded ? (
|
||||
{!isDownloading && !isDownloaded ? (
|
||||
<DownloadCloudIcon
|
||||
size={18}
|
||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||
|
||||
78
web/helpers/atoms/Extension.atom.test.ts
Normal file
78
web/helpers/atoms/Extension.atom.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
// Extension.atom.test.ts
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import * as ExtensionAtoms from './Extension.atom'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
describe('Extension.atom.ts', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('installingExtensionAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
expect(result.current).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('setInstallingExtensionAtom', () => {
|
||||
it('should add a new installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 0 }])
|
||||
})
|
||||
|
||||
it('should update an existing installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 50 })
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 50 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeInstallingExtensionAtom', () => {
|
||||
it('should remove an installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: removeAtom } = renderHook(() => useSetAtom(ExtensionAtoms.removeInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
setAtom.current('ext2', { extensionId: 'ext2', percentage: 50 })
|
||||
removeAtom.current('ext1')
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext2', percentage: 50 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('inActiveEngineProviderAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||
expect(result.current).toEqual([])
|
||||
})
|
||||
|
||||
it('should persist value in storage', () => {
|
||||
const { result } = renderHook(() => useAtom(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||
|
||||
act(() => {
|
||||
result.current[1](['provider1', 'provider2'])
|
||||
})
|
||||
|
||||
// Simulate a re-render to check if the value persists
|
||||
const { result: newResult } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||
expect(newResult.current).toEqual(['provider1', 'provider2'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,4 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import * as ModelAtoms from './Model.atom'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
@ -24,11 +24,6 @@ describe('Model.atom.ts', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('activeAssistantModelAtom', () => {
|
||||
it('should initialize as undefined', () => {
|
||||
expect(ModelAtoms.activeAssistantModelAtom.init).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectedModelAtom', () => {
|
||||
it('should initialize as undefined', () => {
|
||||
|
||||
@ -1,8 +1,59 @@
|
||||
import { ImportingModel, InferenceEngine, Model, ModelFile } from '@janhq/core'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
/**
|
||||
* Enum for the keys used to store models in the local storage.
|
||||
*/
|
||||
enum ModelStorageAtomKeys {
|
||||
DownloadedModels = 'downloadedModels',
|
||||
AvailableModels = 'availableModels',
|
||||
}
|
||||
//// Models Atom
|
||||
/**
|
||||
* Downloaded Models Atom
|
||||
* This atom stores the list of models that have been downloaded.
|
||||
*/
|
||||
export const downloadedModelsAtom = atomWithStorage<ModelFile[]>(
|
||||
ModelStorageAtomKeys.DownloadedModels,
|
||||
[]
|
||||
)
|
||||
|
||||
/**
|
||||
* Configured Models Atom
|
||||
* This atom stores the list of models that have been configured and available to download
|
||||
*/
|
||||
export const configuredModelsAtom = atomWithStorage<ModelFile[]>(
|
||||
ModelStorageAtomKeys.AvailableModels,
|
||||
[]
|
||||
)
|
||||
|
||||
export const removeDownloadedModelAtom = atom(
|
||||
null,
|
||||
(get, set, modelId: string) => {
|
||||
const downloadedModels = get(downloadedModelsAtom)
|
||||
|
||||
set(
|
||||
downloadedModelsAtom,
|
||||
downloadedModels.filter((e) => e.id !== modelId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Atom to store the selected model (from ModelDropdown)
|
||||
*/
|
||||
export const selectedModelAtom = atom<ModelFile | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Atom to store the expanded engine sections (from ModelDropdown)
|
||||
*/
|
||||
export const showEngineListModelAtom = atom<string[]>([InferenceEngine.nitro])
|
||||
|
||||
/// End Models Atom
|
||||
/// Model Download Atom
|
||||
|
||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Stores the list of models which are being downloaded.
|
||||
@ -30,28 +81,20 @@ export const removeDownloadingModelAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
export const downloadedModelsAtom = atom<ModelFile[]>([])
|
||||
|
||||
export const removeDownloadedModelAtom = atom(
|
||||
null,
|
||||
(get, set, modelId: string) => {
|
||||
const downloadedModels = get(downloadedModelsAtom)
|
||||
|
||||
set(
|
||||
downloadedModelsAtom,
|
||||
downloadedModels.filter((e) => e.id !== modelId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const configuredModelsAtom = atom<ModelFile[]>([])
|
||||
|
||||
export const defaultModelAtom = atom<Model | undefined>(undefined)
|
||||
/// End Model Download Atom
|
||||
/// Model Import Atom
|
||||
|
||||
/// TODO: move this part to another atom
|
||||
// store the paths of the models that are being imported
|
||||
export const importingModelsAtom = atom<ImportingModel[]>([])
|
||||
|
||||
// DEPRECATED: Remove when moving to cortex.cpp
|
||||
// Default model template when importing
|
||||
export const defaultModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Importing progress Atom
|
||||
*/
|
||||
export const updateImportingModelProgressAtom = atom(
|
||||
null,
|
||||
(get, set, importId: string, percentage: number) => {
|
||||
@ -69,6 +112,9 @@ export const updateImportingModelProgressAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Importing error Atom
|
||||
*/
|
||||
export const setImportingModelErrorAtom = atom(
|
||||
null,
|
||||
(get, set, importId: string, error: string) => {
|
||||
@ -87,6 +133,9 @@ export const setImportingModelErrorAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Importing success Atom
|
||||
*/
|
||||
export const setImportingModelSuccessAtom = atom(
|
||||
null,
|
||||
(get, set, importId: string, modelId: string) => {
|
||||
@ -105,6 +154,9 @@ export const setImportingModelSuccessAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Update importing model metadata Atom
|
||||
*/
|
||||
export const updateImportingModelAtom = atom(
|
||||
null,
|
||||
(
|
||||
@ -131,6 +183,9 @@ export const updateImportingModelAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
export const selectedModelAtom = atom<ModelFile | undefined>(undefined)
|
||||
/// End Model Import Atom
|
||||
|
||||
export const showEngineListModelAtom = atom<string[]>([InferenceEngine.nitro])
|
||||
/// ModelDropdown States Atom
|
||||
export const isDownloadALocalModelAtom = atom<boolean>(false)
|
||||
export const isAnyRemoteModelConfiguredAtom = atom<boolean>(false)
|
||||
/// End ModelDropdown States Atom
|
||||
|
||||
146
web/helpers/atoms/SystemBar.atom.test.ts
Normal file
146
web/helpers/atoms/SystemBar.atom.test.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import * as SystemBarAtoms from './SystemBar.atom'
|
||||
|
||||
describe('SystemBar.atom.ts', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('totalRamAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
|
||||
act(() => {
|
||||
result.current[1](16384)
|
||||
})
|
||||
expect(result.current[0]).toBe(16384)
|
||||
})
|
||||
})
|
||||
|
||||
describe('usedRamAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
|
||||
act(() => {
|
||||
result.current[1](8192)
|
||||
})
|
||||
expect(result.current[0]).toBe(8192)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cpuUsageAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
|
||||
act(() => {
|
||||
result.current[1](50)
|
||||
})
|
||||
expect(result.current[0]).toBe(50)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ramUtilitizedAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.ramUtilitizedAtom)
|
||||
)
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.ramUtilitizedAtom)
|
||||
)
|
||||
act(() => {
|
||||
result.current[1](75)
|
||||
})
|
||||
expect(result.current[0]).toBe(75)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gpusAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
|
||||
expect(result.current[0]).toEqual([])
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
|
||||
const gpus = [{ id: 'gpu1' }, { id: 'gpu2' }]
|
||||
act(() => {
|
||||
result.current[1](gpus as any)
|
||||
})
|
||||
expect(result.current[0]).toEqual(gpus)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nvidiaTotalVramAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
|
||||
)
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
|
||||
)
|
||||
act(() => {
|
||||
result.current[1](8192)
|
||||
})
|
||||
expect(result.current[0]).toBe(8192)
|
||||
})
|
||||
})
|
||||
|
||||
describe('availableVramAtom', () => {
|
||||
it('should initialize as 0', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.availableVramAtom)
|
||||
)
|
||||
expect(result.current[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.availableVramAtom)
|
||||
)
|
||||
act(() => {
|
||||
result.current[1](4096)
|
||||
})
|
||||
expect(result.current[0]).toBe(4096)
|
||||
})
|
||||
})
|
||||
|
||||
describe('systemMonitorCollapseAtom', () => {
|
||||
it('should initialize as false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
|
||||
)
|
||||
expect(result.current[0]).toBe(false)
|
||||
})
|
||||
|
||||
it('should update correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
|
||||
)
|
||||
act(() => {
|
||||
result.current[1](true)
|
||||
})
|
||||
expect(result.current[0]).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
187
web/helpers/atoms/Thread.atom.test.ts
Normal file
187
web/helpers/atoms/Thread.atom.test.ts
Normal file
@ -0,0 +1,187 @@
|
||||
// Thread.atom.test.ts
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import * as ThreadAtoms from './Thread.atom'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
describe('Thread.atom.ts', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('threadStatesAtom', () => {
|
||||
it('should initialize as an empty object', () => {
|
||||
const { result: threadStatesAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadsAtom)
|
||||
)
|
||||
expect(threadStatesAtom.current[0]).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('threadsAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result: threadsAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadsAtom)
|
||||
)
|
||||
expect(threadsAtom.current[0]).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('threadDataReadyAtom', () => {
|
||||
it('should initialize as false', () => {
|
||||
const { result: threadDataReadyAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadsAtom)
|
||||
)
|
||||
expect(threadDataReadyAtom.current[0]).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('activeThreadIdAtom', () => {
|
||||
it('should set and get active thread id', () => {
|
||||
const { result: getAtom } = renderHook(() =>
|
||||
useAtomValue(ThreadAtoms.getActiveThreadIdAtom)
|
||||
)
|
||||
const { result: setAtom } = renderHook(() =>
|
||||
useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
|
||||
)
|
||||
|
||||
expect(getAtom.current).toBeUndefined()
|
||||
|
||||
act(() => {
|
||||
setAtom.current('thread-1')
|
||||
})
|
||||
|
||||
expect(getAtom.current).toBe('thread-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('activeThreadAtom', () => {
|
||||
it('should return the active thread', () => {
|
||||
const { result: threadsAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadsAtom)
|
||||
)
|
||||
const { result: setActiveThreadId } = renderHook(() =>
|
||||
useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
|
||||
)
|
||||
const { result: activeThread } = renderHook(() =>
|
||||
useAtomValue(ThreadAtoms.activeThreadAtom)
|
||||
)
|
||||
|
||||
act(() => {
|
||||
threadsAtom.current[1]([
|
||||
{ id: 'thread-1', title: 'Test Thread' },
|
||||
] as any)
|
||||
setActiveThreadId.current('thread-1')
|
||||
})
|
||||
|
||||
expect(activeThread.current).toEqual({
|
||||
id: 'thread-1',
|
||||
title: 'Test Thread',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateThreadAtom', () => {
|
||||
it('should update an existing thread', () => {
|
||||
const { result: threadsAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadsAtom)
|
||||
)
|
||||
const { result: updateThread } = renderHook(() =>
|
||||
useSetAtom(ThreadAtoms.updateThreadAtom)
|
||||
)
|
||||
|
||||
act(() => {
|
||||
threadsAtom.current[1]([
|
||||
{
|
||||
id: 'thread-1',
|
||||
title: 'Old Title',
|
||||
updated: new Date('2023-01-01').toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'thread-2',
|
||||
title: 'Thread 2',
|
||||
updated: new Date('2023-01-02').toISOString(),
|
||||
},
|
||||
] as any)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
updateThread.current({
|
||||
id: 'thread-1',
|
||||
title: 'New Title',
|
||||
updated: new Date('2023-01-03').toISOString(),
|
||||
} as any)
|
||||
})
|
||||
|
||||
expect(threadsAtom.current[0]).toEqual([
|
||||
{
|
||||
id: 'thread-1',
|
||||
title: 'New Title',
|
||||
updated: new Date('2023-01-03').toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'thread-2',
|
||||
title: 'Thread 2',
|
||||
updated: new Date('2023-01-02').toISOString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('setThreadModelParamsAtom', () => {
|
||||
it('should set thread model params', () => {
|
||||
const { result: paramsAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadModelParamsAtom)
|
||||
)
|
||||
const { result: setParams } = renderHook(() =>
|
||||
useSetAtom(ThreadAtoms.setThreadModelParamsAtom)
|
||||
)
|
||||
|
||||
act(() => {
|
||||
setParams.current('thread-1', { modelName: 'gpt-3' } as any)
|
||||
})
|
||||
|
||||
expect(paramsAtom.current[0]).toEqual({
|
||||
'thread-1': { modelName: 'gpt-3' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteThreadStateAtom', () => {
|
||||
it('should delete a thread state', () => {
|
||||
const { result: statesAtom } = renderHook(() =>
|
||||
useAtom(ThreadAtoms.threadStatesAtom)
|
||||
)
|
||||
const { result: deleteState } = renderHook(() =>
|
||||
useSetAtom(ThreadAtoms.deleteThreadStateAtom)
|
||||
)
|
||||
|
||||
act(() => {
|
||||
statesAtom.current[1]({
|
||||
'thread-1': { lastMessage: 'Hello' },
|
||||
'thread-2': { lastMessage: 'Hi' },
|
||||
} as any)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
deleteState.current('thread-1')
|
||||
})
|
||||
|
||||
expect(statesAtom.current[0]).toEqual({
|
||||
'thread-2': { lastMessage: 'Hi' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('modalActionThreadAtom', () => {
|
||||
it('should initialize with undefined values', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAtomValue(ThreadAtoms.modalActionThreadAtom)
|
||||
)
|
||||
expect(result.current).toEqual({
|
||||
showModal: undefined,
|
||||
thread: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,45 +1,91 @@
|
||||
import {
|
||||
ModelRuntimeParams,
|
||||
ModelSettingParams,
|
||||
Thread,
|
||||
ThreadContent,
|
||||
ThreadState,
|
||||
} from '@janhq/core'
|
||||
import { Thread, ThreadContent, ThreadState } from '@janhq/core'
|
||||
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
import { ModelParams } from '@/types/model'
|
||||
|
||||
/**
|
||||
* Thread Modal Action Enum
|
||||
*/
|
||||
export enum ThreadModalAction {
|
||||
Clean = 'clean',
|
||||
Delete = 'delete',
|
||||
EditTitle = 'edit-title',
|
||||
}
|
||||
|
||||
export const engineParamsUpdateAtom = atom<boolean>(false)
|
||||
const ACTIVE_SETTING_INPUT_BOX = 'activeSettingInputBox'
|
||||
|
||||
/**
|
||||
* Enum for the keys used to store models in the local storage.
|
||||
*/
|
||||
enum ThreadStorageAtomKeys {
|
||||
ThreadStates = 'threadStates',
|
||||
ThreadList = 'threadList',
|
||||
ThreadListReady = 'threadListReady',
|
||||
}
|
||||
|
||||
//// Threads Atom
|
||||
/**
|
||||
* Stores all thread states for the current user
|
||||
*/
|
||||
export const threadStatesAtom = atomWithStorage<Record<string, ThreadState>>(
|
||||
ThreadStorageAtomKeys.ThreadStates,
|
||||
{}
|
||||
)
|
||||
|
||||
/**
|
||||
* Stores all threads for the current user
|
||||
*/
|
||||
export const threadsAtom = atomWithStorage<Thread[]>(
|
||||
ThreadStorageAtomKeys.ThreadList,
|
||||
[]
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether thread data is ready or not
|
||||
* */
|
||||
export const threadDataReadyAtom = atomWithStorage<boolean>(
|
||||
ThreadStorageAtomKeys.ThreadListReady,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* Store model params at thread level settings
|
||||
*/
|
||||
export const threadModelParamsAtom = atom<Record<string, ModelParams>>({})
|
||||
|
||||
//// End Thread Atom
|
||||
|
||||
/// Active Thread Atom
|
||||
/**
|
||||
* Stores the current active thread id.
|
||||
*/
|
||||
const activeThreadIdAtom = atom<string | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Get the active thread id
|
||||
*/
|
||||
export const getActiveThreadIdAtom = atom((get) => get(activeThreadIdAtom))
|
||||
|
||||
/**
|
||||
* Set the active thread id
|
||||
*/
|
||||
export const setActiveThreadIdAtom = atom(
|
||||
null,
|
||||
(_get, set, threadId: string | undefined) => set(activeThreadIdAtom, threadId)
|
||||
)
|
||||
|
||||
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
||||
|
||||
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
||||
/**
|
||||
* Stores all thread states for the current user
|
||||
* Get the current active thread metadata
|
||||
*/
|
||||
export const threadStatesAtom = atom<Record<string, ThreadState>>({})
|
||||
|
||||
// Whether thread data is ready or not
|
||||
export const threadDataReadyAtom = atom<boolean>(false)
|
||||
export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
||||
get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the active thread state
|
||||
*/
|
||||
export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
const threadId = get(activeThreadIdAtom)
|
||||
if (!threadId) {
|
||||
@ -50,6 +96,38 @@ export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
return get(threadStatesAtom)[threadId]
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the active thread model params
|
||||
*/
|
||||
export const getActiveThreadModelParamsAtom = atom<ModelParams | undefined>(
|
||||
(get) => {
|
||||
const threadId = get(activeThreadIdAtom)
|
||||
if (!threadId) {
|
||||
console.debug('Active thread id is undefined')
|
||||
return undefined
|
||||
}
|
||||
|
||||
return get(threadModelParamsAtom)[threadId]
|
||||
}
|
||||
)
|
||||
/// End Active Thread Atom
|
||||
|
||||
/// Threads State Atom
|
||||
export const engineParamsUpdateAtom = atom<boolean>(false)
|
||||
|
||||
/**
|
||||
* Whether the thread is waiting to send a message
|
||||
*/
|
||||
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Whether the thread is generating a response
|
||||
*/
|
||||
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Remove a thread state from the atom
|
||||
*/
|
||||
export const deleteThreadStateAtom = atom(
|
||||
null,
|
||||
(get, set, threadId: string) => {
|
||||
@ -59,6 +137,9 @@ export const deleteThreadStateAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Update the thread state with the new state
|
||||
*/
|
||||
export const updateThreadWaitingForResponseAtom = atom(
|
||||
null,
|
||||
(get, set, threadId: string, waitingForResponse: boolean) => {
|
||||
@ -71,6 +152,10 @@ export const updateThreadWaitingForResponseAtom = atom(
|
||||
set(threadStatesAtom, currentState)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Update the thread last message
|
||||
*/
|
||||
export const updateThreadStateLastMessageAtom = atom(
|
||||
null,
|
||||
(get, set, threadId: string, lastContent?: ThreadContent[]) => {
|
||||
@ -84,6 +169,9 @@ export const updateThreadStateLastMessageAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Update a thread with the new thread metadata
|
||||
*/
|
||||
export const updateThreadAtom = atom(
|
||||
null,
|
||||
(get, set, updatedThread: Thread) => {
|
||||
@ -103,33 +191,8 @@ export const updateThreadAtom = atom(
|
||||
)
|
||||
|
||||
/**
|
||||
* Stores all threads for the current user
|
||||
* Update the thread model params
|
||||
*/
|
||||
export const threadsAtom = atom<Thread[]>([])
|
||||
|
||||
export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
||||
get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
|
||||
)
|
||||
|
||||
/**
|
||||
* Store model params at thread level settings
|
||||
*/
|
||||
export const threadModelParamsAtom = atom<Record<string, ModelParams>>({})
|
||||
|
||||
export type ModelParams = ModelRuntimeParams | ModelSettingParams
|
||||
|
||||
export const getActiveThreadModelParamsAtom = atom<ModelParams | undefined>(
|
||||
(get) => {
|
||||
const threadId = get(activeThreadIdAtom)
|
||||
if (!threadId) {
|
||||
console.debug('Active thread id is undefined')
|
||||
return undefined
|
||||
}
|
||||
|
||||
return get(threadModelParamsAtom)[threadId]
|
||||
}
|
||||
)
|
||||
|
||||
export const setThreadModelParamsAtom = atom(
|
||||
null,
|
||||
(get, set, threadId: string, params: ModelParams) => {
|
||||
@ -139,12 +202,17 @@ export const setThreadModelParamsAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
const ACTIVE_SETTING_INPUT_BOX = 'activeSettingInputBox'
|
||||
/**
|
||||
* Settings input box active state
|
||||
*/
|
||||
export const activeSettingInputBoxAtom = atomWithStorage<boolean>(
|
||||
ACTIVE_SETTING_INPUT_BOX,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether thread thread is presenting a Modal or not
|
||||
*/
|
||||
export const modalActionThreadAtom = atom<{
|
||||
showModal: ThreadModalAction | undefined
|
||||
thread: Thread | undefined
|
||||
@ -153,5 +221,4 @@ export const modalActionThreadAtom = atom<{
|
||||
thread: undefined,
|
||||
})
|
||||
|
||||
export const isDownloadALocalModelAtom = atom<boolean>(false)
|
||||
export const isAnyRemoteModelConfiguredAtom = atom<boolean>(false)
|
||||
/// Ebd Threads State Atom
|
||||
|
||||
95
web/hooks/useAssistant.test.ts
Normal file
95
web/hooks/useAssistant.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { events, AssistantEvent, ExtensionTypeEnum } from '@janhq/core'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
jest.mock('@janhq/core')
|
||||
jest.mock('@/extension')
|
||||
|
||||
import useAssistants from './useAssistants'
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
// Mock data
|
||||
const mockAssistants = [
|
||||
{ id: 'assistant-1', name: 'Assistant 1' },
|
||||
{ id: 'assistant-2', name: 'Assistant 2' },
|
||||
]
|
||||
|
||||
const mockAssistantExtension = {
|
||||
getAssistants: jest.fn().mockResolvedValue(mockAssistants),
|
||||
} as any
|
||||
|
||||
describe('useAssistants', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.spyOn(extensionManager, 'get').mockReturnValue(mockAssistantExtension)
|
||||
})
|
||||
|
||||
it('should fetch and set assistants on mount', async () => {
|
||||
const mockSetAssistants = jest.fn()
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||
|
||||
renderHook(() => useAssistants())
|
||||
|
||||
// Wait for useEffect to complete
|
||||
await act(async () => {})
|
||||
|
||||
expect(mockAssistantExtension.getAssistants).toHaveBeenCalled()
|
||||
expect(mockSetAssistants).toHaveBeenCalledWith(mockAssistants)
|
||||
})
|
||||
|
||||
it('should update assistants when AssistantEvent.OnAssistantsUpdate is emitted', async () => {
|
||||
const mockSetAssistants = jest.fn()
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||
|
||||
renderHook(() => useAssistants())
|
||||
|
||||
// Wait for initial useEffect to complete
|
||||
await act(async () => {})
|
||||
|
||||
// Clear previous calls
|
||||
mockSetAssistants.mockClear()
|
||||
|
||||
// Simulate AssistantEvent.OnAssistantsUpdate event
|
||||
await act(async () => {
|
||||
events.emit(AssistantEvent.OnAssistantsUpdate, '')
|
||||
})
|
||||
|
||||
expect(mockAssistantExtension.getAssistants).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should unsubscribe from events on unmount', async () => {
|
||||
const { unmount } = renderHook(() => useAssistants())
|
||||
|
||||
// Wait for useEffect to complete
|
||||
await act(async () => {})
|
||||
|
||||
const offSpy = jest.spyOn(events, 'off')
|
||||
|
||||
unmount()
|
||||
|
||||
expect(offSpy).toHaveBeenCalledWith(
|
||||
AssistantEvent.OnAssistantsUpdate,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle case when AssistantExtension is not available', async () => {
|
||||
const mockSetAssistants = jest.fn()
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(undefined)
|
||||
|
||||
renderHook(() => useAssistants())
|
||||
|
||||
// Wait for useEffect to complete
|
||||
await act(async () => {})
|
||||
|
||||
expect(mockSetAssistants).toHaveBeenCalledWith([])
|
||||
})
|
||||
})
|
||||
105
web/hooks/useClipboard.test.ts
Normal file
105
web/hooks/useClipboard.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useClipboard } from './useClipboard'
|
||||
|
||||
describe('useClipboard', () => {
|
||||
let originalClipboard: any
|
||||
|
||||
beforeAll(() => {
|
||||
originalClipboard = { ...global.navigator.clipboard }
|
||||
const mockClipboard = {
|
||||
writeText: jest.fn(() => Promise.resolve()),
|
||||
}
|
||||
// @ts-ignore
|
||||
global.navigator.clipboard = mockClipboard
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
global.navigator.clipboard = originalClipboard
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should copy text to clipboard', async () => {
|
||||
const { result } = renderHook(() => useClipboard())
|
||||
|
||||
await act(async () => {
|
||||
result.current.copy('Test text')
|
||||
})
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Test text')
|
||||
expect(result.current.copied).toBe(true)
|
||||
expect(result.current.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should set copied to false after timeout', async () => {
|
||||
const { result } = renderHook(() => useClipboard({ timeout: 1000 }))
|
||||
|
||||
await act(async () => {
|
||||
result.current.copy('Test text')
|
||||
})
|
||||
|
||||
expect(result.current.copied).toBe(true)
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
expect(result.current.copied).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle clipboard errors', async () => {
|
||||
const mockError = new Error('Clipboard error')
|
||||
// @ts-ignore
|
||||
navigator.clipboard.writeText.mockRejectedValueOnce(mockError)
|
||||
|
||||
const { result } = renderHook(() => useClipboard())
|
||||
|
||||
await act(async () => {
|
||||
result.current.copy('Test text')
|
||||
})
|
||||
|
||||
expect(result.current.error).toEqual(mockError)
|
||||
expect(result.current.copied).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset state', async () => {
|
||||
const { result } = renderHook(() => useClipboard())
|
||||
|
||||
await act(async () => {
|
||||
result.current.copy('Test text')
|
||||
})
|
||||
|
||||
expect(result.current.copied).toBe(true)
|
||||
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
|
||||
expect(result.current.copied).toBe(false)
|
||||
expect(result.current.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle missing clipboard API', () => {
|
||||
// @ts-ignore
|
||||
delete global.navigator.clipboard
|
||||
|
||||
const { result } = renderHook(() => useClipboard())
|
||||
|
||||
act(() => {
|
||||
result.current.copy('Test text')
|
||||
})
|
||||
|
||||
expect(result.current.error).toEqual(
|
||||
new Error('useClipboard: navigator.clipboard is not supported')
|
||||
)
|
||||
expect(result.current.copied).toBe(false)
|
||||
})
|
||||
})
|
||||
73
web/hooks/useDeleteModel.test.ts
Normal file
73
web/hooks/useDeleteModel.test.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import useDeleteModel from './useDeleteModel'
|
||||
import { toaster } from '@/containers/Toast'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('@/extension/ExtensionManager')
|
||||
jest.mock('@/containers/Toast')
|
||||
jest.mock('jotai', () => ({
|
||||
useSetAtom: jest.fn(() => jest.fn()),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('useDeleteModel', () => {
|
||||
const mockModel: any = {
|
||||
id: 'test-model',
|
||||
name: 'Test Model',
|
||||
// Add other required properties of ModelFile
|
||||
}
|
||||
|
||||
const mockDeleteModel = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
deleteModel: mockDeleteModel,
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a model successfully', async () => {
|
||||
const { result } = renderHook(() => useDeleteModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteModel(mockModel)
|
||||
})
|
||||
|
||||
expect(mockDeleteModel).toHaveBeenCalledWith(mockModel)
|
||||
expect(toaster).toHaveBeenCalledWith({
|
||||
title: 'Model Deletion Successful',
|
||||
description: `Model ${mockModel.name} has been successfully deleted.`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call removeDownloadedModel with the model id', async () => {
|
||||
const { result } = renderHook(() => useDeleteModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteModel(mockModel)
|
||||
})
|
||||
|
||||
// Assuming useSetAtom returns a mock function
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||
expect(useSetAtom).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle errors during model deletion', async () => {
|
||||
const error = new Error('Deletion failed')
|
||||
mockDeleteModel.mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useDeleteModel())
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.deleteModel(mockModel)).rejects.toThrow(
|
||||
'Deletion failed'
|
||||
)
|
||||
})
|
||||
|
||||
expect(mockDeleteModel).toHaveBeenCalledWith(mockModel)
|
||||
expect(toaster).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
106
web/hooks/useDeleteThread.test.ts
Normal file
106
web/hooks/useDeleteThread.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import useDeleteThread from './useDeleteThread'
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
// Mock the necessary dependencies
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/extension/ExtensionManager')
|
||||
jest.mock('@/containers/Toast')
|
||||
|
||||
describe('useDeleteThread', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should delete a thread successfully', async () => {
|
||||
const mockThreads = [
|
||||
{ id: 'thread1', title: 'Thread 1' },
|
||||
{ id: 'thread2', title: 'Thread 2' },
|
||||
]
|
||||
const mockSetThreads = jest.fn()
|
||||
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||
|
||||
const mockDeleteThread = jest.fn()
|
||||
extensionManager.get = jest.fn().mockReturnValue({
|
||||
deleteThread: mockDeleteThread,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDeleteThread())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteThread('thread1')
|
||||
})
|
||||
|
||||
expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
|
||||
expect(mockSetThreads).toHaveBeenCalledWith([mockThreads[1]])
|
||||
})
|
||||
|
||||
it('should clean a thread successfully', async () => {
|
||||
const mockThreads = [{ id: 'thread1', title: 'Thread 1', metadata: {} }]
|
||||
const mockSetThreads = jest.fn()
|
||||
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||
const mockCleanMessages = jest.fn()
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(() => mockCleanMessages)
|
||||
;(useAtomValue as jest.Mock).mockReturnValue(['thread 1'])
|
||||
|
||||
const mockWriteMessages = jest.fn()
|
||||
const mockSaveThread = jest.fn()
|
||||
extensionManager.get = jest.fn().mockReturnValue({
|
||||
writeMessages: mockWriteMessages,
|
||||
saveThread: mockSaveThread,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDeleteThread())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.cleanThread('thread1')
|
||||
})
|
||||
|
||||
expect(mockWriteMessages).toHaveBeenCalled()
|
||||
expect(mockSaveThread).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'thread1',
|
||||
title: 'New Thread',
|
||||
metadata: expect.objectContaining({ lastMessage: undefined }),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle errors when deleting a thread', async () => {
|
||||
const mockThreads = [{ id: 'thread1', title: 'Thread 1' }]
|
||||
const mockSetThreads = jest.fn()
|
||||
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||
|
||||
const mockDeleteThread = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Delete error'))
|
||||
extensionManager.get = jest.fn().mockReturnValue({
|
||||
deleteThread: mockDeleteThread,
|
||||
})
|
||||
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { result } = renderHook(() => useDeleteThread())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteThread('thread1')
|
||||
})
|
||||
|
||||
expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error))
|
||||
expect(mockSetThreads).not.toHaveBeenCalled()
|
||||
expect(toaster).not.toHaveBeenCalled()
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
98
web/hooks/useDownloadModel.test.ts
Normal file
98
web/hooks/useDownloadModel.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useAtom, useSetAtom } from 'jotai'
|
||||
import useDownloadModel from './useDownloadModel'
|
||||
import * as core from '@janhq/core'
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
|
||||
// Mock the necessary dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
jest.mock('@janhq/core')
|
||||
jest.mock('@/extension/ExtensionManager')
|
||||
jest.mock('./useGpuSetting', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
getGpuSettings: jest.fn().mockResolvedValue({ some: 'gpuSettings' }),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useDownloadModel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useAtom as jest.Mock).mockReturnValue([false, jest.fn()])
|
||||
})
|
||||
|
||||
it('should download a model', async () => {
|
||||
const mockModel: core.Model = {
|
||||
id: 'test-model',
|
||||
sources: [{ filename: 'test.bin' }],
|
||||
} as core.Model
|
||||
|
||||
const mockExtension = {
|
||||
downloadModel: jest.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
|
||||
|
||||
const { result } = renderHook(() => useDownloadModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.downloadModel(mockModel)
|
||||
})
|
||||
|
||||
expect(mockExtension.downloadModel).toHaveBeenCalledWith(
|
||||
mockModel,
|
||||
{ some: 'gpuSettings' },
|
||||
{ ignoreSSL: undefined, proxy: '' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should abort model download', async () => {
|
||||
const mockModel: core.Model = {
|
||||
id: 'test-model',
|
||||
sources: [{ filename: 'test.bin' }],
|
||||
} as core.Model
|
||||
|
||||
;(core.joinPath as jest.Mock).mockResolvedValue('/path/to/model/test.bin')
|
||||
;(core.abortDownload as jest.Mock).mockResolvedValue(undefined)
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||
const { result } = renderHook(() => useDownloadModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.abortModelDownload(mockModel)
|
||||
})
|
||||
|
||||
expect(core.abortDownload).toHaveBeenCalledWith('/path/to/model/test.bin')
|
||||
})
|
||||
|
||||
it('should handle proxy settings', async () => {
|
||||
const mockModel: core.Model = {
|
||||
id: 'test-model',
|
||||
sources: [{ filename: 'test.bin' }],
|
||||
} as core.Model
|
||||
|
||||
const mockExtension = {
|
||||
downloadModel: jest.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
|
||||
;(useAtom as jest.Mock).mockReturnValueOnce([true, jest.fn()]) // proxyEnabled
|
||||
;(useAtom as jest.Mock).mockReturnValueOnce(['http://proxy.com', jest.fn()]) // proxy
|
||||
|
||||
const { result } = renderHook(() => useDownloadModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.downloadModel(mockModel)
|
||||
})
|
||||
|
||||
expect(mockExtension.downloadModel).toHaveBeenCalledWith(
|
||||
mockModel,
|
||||
expect.objectContaining({ some: 'gpuSettings' }),
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
129
web/hooks/useDropModelBinaries.test.ts
Normal file
129
web/hooks/useDropModelBinaries.test.ts
Normal file
@ -0,0 +1,129 @@
|
||||
// useDropModelBinaries.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import useDropModelBinaries from './useDropModelBinaries'
|
||||
import { getFileInfoFromFile } from '@/utils/file'
|
||||
import { snackbar } from '@/containers/Toast'
|
||||
|
||||
// Mock dependencies
|
||||
// Mock the necessary dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
jest.mock('uuid')
|
||||
jest.mock('@/utils/file')
|
||||
jest.mock('@/containers/Toast')
|
||||
|
||||
describe('useDropModelBinaries', () => {
|
||||
const mockSetImportingModels = jest.fn()
|
||||
const mockSetImportModelStage = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportingModels)
|
||||
;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportModelStage)
|
||||
;(uuidv4 as jest.Mock).mockReturnValue('mock-uuid')
|
||||
;(getFileInfoFromFile as jest.Mock).mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('should handle dropping supported files', async () => {
|
||||
const { result } = renderHook(() => useDropModelBinaries())
|
||||
|
||||
const mockFiles = [
|
||||
{ name: 'model1.gguf', path: '/path/to/model1.gguf', size: 1000 },
|
||||
{ name: 'model2.gguf', path: '/path/to/model2.gguf', size: 2000 },
|
||||
]
|
||||
|
||||
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onDropModels([])
|
||||
})
|
||||
|
||||
expect(mockSetImportingModels).toHaveBeenCalledWith([
|
||||
{
|
||||
importId: 'mock-uuid',
|
||||
modelId: undefined,
|
||||
name: 'model1',
|
||||
description: '',
|
||||
path: '/path/to/model1.gguf',
|
||||
tags: [],
|
||||
size: 1000,
|
||||
status: 'PREPARING',
|
||||
format: 'gguf',
|
||||
},
|
||||
{
|
||||
importId: 'mock-uuid',
|
||||
modelId: undefined,
|
||||
name: 'model2',
|
||||
description: '',
|
||||
path: '/path/to/model2.gguf',
|
||||
tags: [],
|
||||
size: 2000,
|
||||
status: 'PREPARING',
|
||||
format: 'gguf',
|
||||
},
|
||||
])
|
||||
expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
|
||||
})
|
||||
|
||||
it('should handle dropping unsupported files', async () => {
|
||||
const { result } = renderHook(() => useDropModelBinaries())
|
||||
|
||||
const mockFiles = [
|
||||
{ name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
|
||||
]
|
||||
|
||||
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onDropModels([])
|
||||
})
|
||||
|
||||
expect(snackbar).toHaveBeenCalledWith({
|
||||
description: 'Only files with .gguf extension can be imported.',
|
||||
type: 'error',
|
||||
})
|
||||
expect(mockSetImportingModels).not.toHaveBeenCalled()
|
||||
expect(mockSetImportModelStage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle dropping both supported and unsupported files', async () => {
|
||||
const { result } = renderHook(() => useDropModelBinaries())
|
||||
|
||||
const mockFiles = [
|
||||
{ name: 'model.gguf', path: '/path/to/model.gguf', size: 1000 },
|
||||
{ name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
|
||||
]
|
||||
|
||||
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onDropModels([])
|
||||
})
|
||||
|
||||
expect(snackbar).toHaveBeenCalledWith({
|
||||
description: 'Only files with .gguf extension can be imported.',
|
||||
type: 'error',
|
||||
})
|
||||
expect(mockSetImportingModels).toHaveBeenCalledWith([
|
||||
{
|
||||
importId: 'mock-uuid',
|
||||
modelId: undefined,
|
||||
name: 'model',
|
||||
description: '',
|
||||
path: '/path/to/model.gguf',
|
||||
tags: [],
|
||||
size: 1000,
|
||||
status: 'PREPARING',
|
||||
format: 'gguf',
|
||||
},
|
||||
])
|
||||
expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
|
||||
})
|
||||
})
|
||||
89
web/hooks/useFactoryReset.test.ts
Normal file
89
web/hooks/useFactoryReset.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import useFactoryReset, { FactoryResetState } from './useFactoryReset'
|
||||
import { useActiveModel } from './useActiveModel'
|
||||
import { fs } from '@janhq/core'
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
atom: jest.fn(),
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
}))
|
||||
jest.mock('./useActiveModel', () => ({
|
||||
useActiveModel: jest.fn(),
|
||||
}))
|
||||
jest.mock('@janhq/core', () => ({
|
||||
fs: {
|
||||
rm: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useFactoryReset', () => {
|
||||
const mockStopModel = jest.fn()
|
||||
const mockSetFactoryResetState = jest.fn()
|
||||
const mockGetAppConfigurations = jest.fn()
|
||||
const mockUpdateAppConfiguration = jest.fn()
|
||||
const mockRelaunch = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useAtomValue as jest.Mock).mockReturnValue('/default/jan/data/folder')
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(mockSetFactoryResetState)
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({ stopModel: mockStopModel })
|
||||
global.window ??= Object.create(window)
|
||||
global.window.core = {
|
||||
api: {
|
||||
getAppConfigurations: mockGetAppConfigurations,
|
||||
updateAppConfiguration: mockUpdateAppConfiguration,
|
||||
relaunch: mockRelaunch,
|
||||
},
|
||||
}
|
||||
mockGetAppConfigurations.mockResolvedValue({
|
||||
data_folder: '/current/jan/data/folder',
|
||||
quick_ask: false,
|
||||
})
|
||||
jest.spyOn(global, 'setTimeout')
|
||||
})
|
||||
|
||||
it('should reset all correctly', async () => {
|
||||
const { result } = renderHook(() => useFactoryReset())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.resetAll()
|
||||
})
|
||||
|
||||
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||
FactoryResetState.Starting
|
||||
)
|
||||
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||
FactoryResetState.StoppingModel
|
||||
)
|
||||
expect(mockStopModel).toHaveBeenCalled()
|
||||
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 4000)
|
||||
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||
FactoryResetState.DeletingData
|
||||
)
|
||||
expect(fs.rm).toHaveBeenCalledWith('/current/jan/data/folder')
|
||||
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||
data_folder: '/default/jan/data/folder',
|
||||
quick_ask: false,
|
||||
})
|
||||
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||
FactoryResetState.ClearLocalStorage
|
||||
)
|
||||
expect(mockRelaunch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep current folder when specified', async () => {
|
||||
const { result } = renderHook(() => useFactoryReset())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.resetAll(true)
|
||||
})
|
||||
|
||||
expect(mockUpdateAppConfiguration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Add more tests as needed for error cases, edge cases, etc.
|
||||
})
|
||||
39
web/hooks/useGetHFRepoData.test.ts
Normal file
39
web/hooks/useGetHFRepoData.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useGetHFRepoData } from './useGetHFRepoData'
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
jest.mock('@/extension', () => ({
|
||||
extensionManager: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useGetHFRepoData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should fetch HF repo data successfully', async () => {
|
||||
const mockData = { name: 'Test Repo', stars: 100 }
|
||||
const mockFetchHuggingFaceRepoData = jest.fn().mockResolvedValue(mockData)
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
fetchHuggingFaceRepoData: mockFetchHuggingFaceRepoData,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGetHFRepoData())
|
||||
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.error).toBeUndefined()
|
||||
|
||||
let data
|
||||
act(() => {
|
||||
data = result.current.getHfRepoData('test-repo')
|
||||
})
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
|
||||
expect(result.current.error).toBeUndefined()
|
||||
expect(await data).toEqual(mockData)
|
||||
expect(mockFetchHuggingFaceRepoData).toHaveBeenCalledWith('test-repo')
|
||||
})
|
||||
})
|
||||
103
web/hooks/useGetSystemResources.test.ts
Normal file
103
web/hooks/useGetSystemResources.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
// useGetSystemResources.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import useGetSystemResources from './useGetSystemResources'
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
|
||||
// Mock the extensionManager
|
||||
jest.mock('@/extension/ExtensionManager', () => ({
|
||||
extensionManager: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the necessary dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: () => jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('useGetSystemResources', () => {
|
||||
const mockMonitoringExtension = {
|
||||
getResourcesInfo: jest.fn(),
|
||||
getCurrentLoad: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(
|
||||
mockMonitoringExtension
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should fetch system resources on initial render', async () => {
|
||||
mockMonitoringExtension.getResourcesInfo.mockResolvedValue({
|
||||
mem: { usedMemory: 4000, totalMemory: 8000 },
|
||||
})
|
||||
mockMonitoringExtension.getCurrentLoad.mockResolvedValue({
|
||||
cpu: { usage: 50 },
|
||||
gpu: [],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGetSystemResources())
|
||||
|
||||
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should start watching system resources when watch is called', () => {
|
||||
const { result } = renderHook(() => useGetSystemResources())
|
||||
|
||||
act(() => {
|
||||
result.current.watch()
|
||||
})
|
||||
|
||||
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||
|
||||
// Fast-forward time by 2 seconds
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(2000)
|
||||
})
|
||||
|
||||
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should stop watching when stopWatching is called', () => {
|
||||
const { result } = renderHook(() => useGetSystemResources())
|
||||
|
||||
act(() => {
|
||||
result.current.watch()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.stopWatching()
|
||||
})
|
||||
|
||||
// Fast-forward time by 2 seconds
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(2000)
|
||||
})
|
||||
|
||||
// Expect no additional calls after stopping
|
||||
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch resources if monitoring extension is not available', async () => {
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(null)
|
||||
|
||||
const { result } = renderHook(() => useGetSystemResources())
|
||||
|
||||
await act(async () => {
|
||||
result.current.getSystemResources()
|
||||
})
|
||||
|
||||
expect(mockMonitoringExtension.getResourcesInfo).not.toHaveBeenCalled()
|
||||
expect(mockMonitoringExtension.getCurrentLoad).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
87
web/hooks/useGpuSetting.test.ts
Normal file
87
web/hooks/useGpuSetting.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// useGpuSetting.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { ExtensionTypeEnum, MonitoringExtension } from '@janhq/core'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/extension')
|
||||
|
||||
import useGpuSetting from './useGpuSetting'
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
describe('useGpuSetting', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return GPU settings when available', async () => {
|
||||
const mockGpuSettings = {
|
||||
gpuCount: 2,
|
||||
gpuNames: ['NVIDIA GeForce RTX 3080', 'NVIDIA GeForce RTX 3070'],
|
||||
totalMemory: 20000,
|
||||
freeMemory: 15000,
|
||||
}
|
||||
|
||||
const mockMonitoringExtension: Partial<MonitoringExtension> = {
|
||||
getGpuSetting: jest.fn().mockResolvedValue(mockGpuSettings),
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(extensionManager, 'get')
|
||||
.mockReturnValue(mockMonitoringExtension as MonitoringExtension)
|
||||
|
||||
const { result } = renderHook(() => useGpuSetting())
|
||||
|
||||
let gpuSettings
|
||||
await act(async () => {
|
||||
gpuSettings = await result.current.getGpuSettings()
|
||||
})
|
||||
|
||||
expect(gpuSettings).toEqual(mockGpuSettings)
|
||||
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||
ExtensionTypeEnum.SystemMonitoring
|
||||
)
|
||||
expect(mockMonitoringExtension.getGpuSetting).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return undefined when no GPU settings are found', async () => {
|
||||
const mockMonitoringExtension: Partial<MonitoringExtension> = {
|
||||
getGpuSetting: jest.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(extensionManager, 'get')
|
||||
.mockReturnValue(mockMonitoringExtension as MonitoringExtension)
|
||||
|
||||
const { result } = renderHook(() => useGpuSetting())
|
||||
|
||||
let gpuSettings
|
||||
await act(async () => {
|
||||
gpuSettings = await result.current.getGpuSettings()
|
||||
})
|
||||
|
||||
expect(gpuSettings).toBeUndefined()
|
||||
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||
ExtensionTypeEnum.SystemMonitoring
|
||||
)
|
||||
expect(mockMonitoringExtension.getGpuSetting).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle missing MonitoringExtension', async () => {
|
||||
jest.spyOn(extensionManager, 'get').mockReturnValue(undefined)
|
||||
jest.spyOn(console, 'debug').mockImplementation(() => {})
|
||||
|
||||
const { result } = renderHook(() => useGpuSetting())
|
||||
|
||||
let gpuSettings
|
||||
await act(async () => {
|
||||
gpuSettings = await result.current.getGpuSettings()
|
||||
})
|
||||
|
||||
expect(gpuSettings).toBeUndefined()
|
||||
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||
ExtensionTypeEnum.SystemMonitoring
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalledWith('No GPU setting found')
|
||||
})
|
||||
})
|
||||
70
web/hooks/useImportModel.test.ts
Normal file
70
web/hooks/useImportModel.test.ts
Normal file
@ -0,0 +1,70 @@
|
||||
// useImportModel.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { extensionManager } from '@/extension'
|
||||
import useImportModel from './useImportModel'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@janhq/core')
|
||||
jest.mock('@/extension')
|
||||
jest.mock('@/containers/Toast')
|
||||
jest.mock('uuid', () => ({ v4: () => 'mocked-uuid' }))
|
||||
|
||||
describe('useImportModel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should import models successfully', async () => {
|
||||
const mockImportModels = jest.fn().mockResolvedValue(undefined)
|
||||
const mockExtension = {
|
||||
importModels: mockImportModels,
|
||||
} as any
|
||||
|
||||
jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
|
||||
|
||||
const { result } = renderHook(() => useImportModel())
|
||||
|
||||
const models = [
|
||||
{ importId: '1', name: 'Model 1', path: '/path/to/model1' },
|
||||
{ importId: '2', name: 'Model 2', path: '/path/to/model2' },
|
||||
] as any
|
||||
|
||||
await act(async () => {
|
||||
await result.current.importModels(models, 'local' as any)
|
||||
})
|
||||
|
||||
expect(mockImportModels).toHaveBeenCalledWith(models, 'local')
|
||||
})
|
||||
|
||||
it('should update model info successfully', async () => {
|
||||
const mockUpdateModelInfo = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: 'model-1', name: 'Updated Model' })
|
||||
const mockExtension = {
|
||||
updateModelInfo: mockUpdateModelInfo,
|
||||
} as any
|
||||
|
||||
jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
|
||||
|
||||
const { result } = renderHook(() => useImportModel())
|
||||
|
||||
const modelInfo = { id: 'model-1', name: 'Updated Model' }
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateModelInfo(modelInfo)
|
||||
})
|
||||
|
||||
expect(mockUpdateModelInfo).toHaveBeenCalledWith(modelInfo)
|
||||
})
|
||||
|
||||
it('should handle empty file paths', async () => {
|
||||
const { result } = renderHook(() => useImportModel())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sanitizeFilePaths([])
|
||||
})
|
||||
|
||||
// Expect no state changes or side effects
|
||||
})
|
||||
})
|
||||
111
web/hooks/useLoadTheme.test.ts
Normal file
111
web/hooks/useLoadTheme.test.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { fs, joinPath } from '@janhq/core'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { useLoadTheme } from './useLoadTheme'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('next-themes')
|
||||
jest.mock('@janhq/core')
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('useLoadTheme', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const mockJanDataFolderPath = '/mock/path'
|
||||
const mockThemesPath = '/mock/path/themes'
|
||||
const mockSelectedThemeId = 'joi-light'
|
||||
const mockThemeData = {
|
||||
id: 'joi-light',
|
||||
displayName: 'Joi Light',
|
||||
nativeTheme: 'light',
|
||||
variables: {
|
||||
'--primary-color': '#007bff',
|
||||
},
|
||||
}
|
||||
|
||||
it('should load theme and set variables', async () => {
|
||||
// Mock Jotai hooks
|
||||
;(useAtomValue as jest.Mock).mockReturnValue(mockJanDataFolderPath)
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||
;(useAtom as jest.Mock).mockReturnValue([mockSelectedThemeId, jest.fn()])
|
||||
;(useAtom as jest.Mock).mockReturnValue([mockThemeData, jest.fn()])
|
||||
|
||||
// Mock fs and joinPath
|
||||
;(fs.readdirSync as jest.Mock).mockResolvedValue(['joi-light', 'joi-dark'])
|
||||
;(fs.readFileSync as jest.Mock).mockResolvedValue(
|
||||
JSON.stringify(mockThemeData)
|
||||
)
|
||||
;(joinPath as jest.Mock).mockImplementation((paths) => paths.join('/'))
|
||||
|
||||
// Mock setTheme from next-themes
|
||||
const mockSetTheme = jest.fn()
|
||||
;(useTheme as jest.Mock).mockReturnValue({ setTheme: mockSetTheme })
|
||||
|
||||
// Mock window.electronAPI
|
||||
Object.defineProperty(window, 'electronAPI', {
|
||||
value: {
|
||||
setNativeThemeLight: jest.fn(),
|
||||
setNativeThemeDark: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useLoadTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
})
|
||||
|
||||
// Assertions
|
||||
expect(fs.readdirSync).toHaveBeenCalledWith(mockThemesPath)
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||
`${mockThemesPath}/${mockSelectedThemeId}/theme.json`,
|
||||
'utf-8'
|
||||
)
|
||||
expect(mockSetTheme).toHaveBeenCalledWith('light')
|
||||
expect(window.electronAPI.setNativeThemeLight).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set default theme if no selected theme', async () => {
|
||||
// Mock Jotai hooks with empty selected theme
|
||||
;(useAtomValue as jest.Mock).mockReturnValue(mockJanDataFolderPath)
|
||||
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||
;(useAtom as jest.Mock).mockReturnValue(['', jest.fn()])
|
||||
;(useAtom as jest.Mock).mockReturnValue([{}, jest.fn()])
|
||||
|
||||
const mockSetSelectedThemeId = jest.fn()
|
||||
;(useAtom as jest.Mock).mockReturnValue(['', mockSetSelectedThemeId])
|
||||
|
||||
const { result } = renderHook(() => useLoadTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
})
|
||||
|
||||
expect(mockSetSelectedThemeId).toHaveBeenCalledWith('joi-light')
|
||||
})
|
||||
|
||||
it('should handle missing janDataFolderPath', async () => {
|
||||
// Mock Jotai hooks with empty janDataFolderPath
|
||||
;(useAtomValue as jest.Mock).mockReturnValue('')
|
||||
|
||||
const { result } = renderHook(() => useLoadTheme())
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
})
|
||||
|
||||
expect(fs.readdirSync).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
103
web/hooks/useLogs.test.ts
Normal file
103
web/hooks/useLogs.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
// useLogs.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { fs, joinPath, openFileExplorer } from '@janhq/core'
|
||||
|
||||
import { useLogs } from './useLogs'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@janhq/core', () => ({
|
||||
fs: {
|
||||
existsSync: jest.fn(),
|
||||
readFileSync: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
},
|
||||
joinPath: jest.fn(),
|
||||
openFileExplorer: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('useLogs', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useAtomValue as jest.Mock).mockReturnValue('/mock/jan/data/folder')
|
||||
})
|
||||
|
||||
it('should get logs and sanitize them', async () => {
|
||||
const mockLogs = '/mock/jan/data/folder/some/log/content'
|
||||
const expectedSanitizedLogs = 'jan-data-folder/some/log/content'
|
||||
|
||||
;(joinPath as jest.Mock).mockResolvedValue('file://logs/test.log')
|
||||
;(fs.existsSync as jest.Mock).mockResolvedValue(true)
|
||||
;(fs.readFileSync as jest.Mock).mockResolvedValue(mockLogs)
|
||||
|
||||
const { result } = renderHook(() => useLogs())
|
||||
|
||||
await act(async () => {
|
||||
const logs = await result.current.getLogs('test')
|
||||
expect(logs).toBe(expectedSanitizedLogs)
|
||||
})
|
||||
|
||||
expect(joinPath).toHaveBeenCalledWith(['file://logs', 'test.log'])
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('file://logs/test.log')
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||
'file://logs/test.log',
|
||||
'utf-8'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return empty string if log file does not exist', async () => {
|
||||
;(joinPath as jest.Mock).mockResolvedValue('file://logs/nonexistent.log')
|
||||
;(fs.existsSync as jest.Mock).mockResolvedValue(false)
|
||||
|
||||
const { result } = renderHook(() => useLogs())
|
||||
|
||||
await act(async () => {
|
||||
const logs = await result.current.getLogs('nonexistent')
|
||||
expect(logs).toBe('')
|
||||
})
|
||||
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open server log', async () => {
|
||||
;(joinPath as jest.Mock).mockResolvedValue(
|
||||
'/mock/jan/data/folder/logs/app.log'
|
||||
)
|
||||
;(openFileExplorer as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
const { result } = renderHook(() => useLogs())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.openServerLog()
|
||||
})
|
||||
|
||||
expect(joinPath).toHaveBeenCalledWith([
|
||||
'/mock/jan/data/folder',
|
||||
'logs',
|
||||
'app.log',
|
||||
])
|
||||
expect(openFileExplorer).toHaveBeenCalledWith(
|
||||
'/mock/jan/data/folder/logs/app.log'
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear server log', async () => {
|
||||
;(joinPath as jest.Mock).mockResolvedValue('file://logs/app.log')
|
||||
;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
const { result } = renderHook(() => useLogs())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.clearServerLog()
|
||||
})
|
||||
|
||||
expect(joinPath).toHaveBeenCalledWith(['file://logs', 'app.log'])
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith('file://logs/app.log', '')
|
||||
})
|
||||
})
|
||||
61
web/hooks/useModels.test.ts
Normal file
61
web/hooks/useModels.test.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// useModels.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { events, ModelEvent } from '@janhq/core'
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@janhq/core')
|
||||
jest.mock('@/extension')
|
||||
|
||||
import useModels from './useModels'
|
||||
|
||||
// Mock data
|
||||
const mockDownloadedModels = [
|
||||
{ id: 'model-1', name: 'Model 1' },
|
||||
{ id: 'model-2', name: 'Model 2' },
|
||||
]
|
||||
|
||||
const mockConfiguredModels = [
|
||||
{ id: 'model-3', name: 'Model 3' },
|
||||
{ id: 'model-4', name: 'Model 4' },
|
||||
]
|
||||
|
||||
const mockDefaultModel = { id: 'default-model', name: 'Default Model' }
|
||||
|
||||
describe('useModels', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should fetch and set models on mount', async () => {
|
||||
const mockModelExtension = {
|
||||
getDownloadedModels: jest.fn().mockResolvedValue(mockDownloadedModels),
|
||||
getConfiguredModels: jest.fn().mockResolvedValue(mockConfiguredModels),
|
||||
getDefaultModel: jest.fn().mockResolvedValue(mockDefaultModel),
|
||||
} as any
|
||||
|
||||
jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() => useModels())
|
||||
})
|
||||
|
||||
expect(mockModelExtension.getDownloadedModels).toHaveBeenCalled()
|
||||
expect(mockModelExtension.getConfiguredModels).toHaveBeenCalled()
|
||||
expect(mockModelExtension.getDefaultModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove event listener on unmount', async () => {
|
||||
const removeListenerSpy = jest.spyOn(events, 'off')
|
||||
|
||||
const { unmount } = renderHook(() => useModels())
|
||||
|
||||
unmount()
|
||||
|
||||
expect(removeListenerSpy).toHaveBeenCalledWith(
|
||||
ModelEvent.OnModelsUpdate,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -18,6 +18,11 @@ import {
|
||||
downloadedModelsAtom,
|
||||
} from '@/helpers/atoms/Model.atom'
|
||||
|
||||
/**
|
||||
* useModels hook - Handles the state of models
|
||||
* It fetches the downloaded models, configured models and default model from Model Extension
|
||||
* and updates the atoms accordingly.
|
||||
*/
|
||||
const useModels = () => {
|
||||
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
||||
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
||||
@ -39,6 +44,7 @@ const useModels = () => {
|
||||
setDefaultModel(defaultModel)
|
||||
}
|
||||
|
||||
// Fetch all data
|
||||
Promise.all([
|
||||
getDownloadedModels(),
|
||||
getConfiguredModels(),
|
||||
@ -59,16 +65,19 @@ const useModels = () => {
|
||||
}, [getData])
|
||||
}
|
||||
|
||||
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||
const getLocalDefaultModel = async (): Promise<Model | undefined> =>
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.getDefaultModel()
|
||||
|
||||
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||
const getLocalConfiguredModels = async (): Promise<ModelFile[]> =>
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.getConfiguredModels() ?? []
|
||||
|
||||
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||
const getLocalDownloadedModels = async (): Promise<ModelFile[]> =>
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
|
||||
@ -8,10 +8,10 @@ import {
|
||||
setConvoMessagesAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
ModelParams,
|
||||
setActiveThreadIdAtom,
|
||||
setThreadModelParamsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
import { ModelParams } from '@/types/model'
|
||||
|
||||
export default function useSetActiveThread() {
|
||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||
|
||||
192
web/hooks/useThread.test.ts
Normal file
192
web/hooks/useThread.test.ts
Normal file
@ -0,0 +1,192 @@
|
||||
// useThreads.test.ts
|
||||
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { ExtensionTypeEnum } from '@janhq/core'
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import useThreads from './useThreads'
|
||||
import {
|
||||
threadDataReadyAtom,
|
||||
threadModelParamsAtom,
|
||||
threadsAtom,
|
||||
threadStatesAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
// Mock the necessary dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
atom: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/extension/ExtensionManager')
|
||||
|
||||
describe('useThreads', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const mockThreads = [
|
||||
{
|
||||
id: 'thread1',
|
||||
metadata: { lastMessage: 'Hello' },
|
||||
assistants: [
|
||||
{
|
||||
model: {
|
||||
parameters: { param1: 'value1' },
|
||||
settings: { setting1: 'value1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'thread2',
|
||||
metadata: { lastMessage: 'Hi there' },
|
||||
assistants: [
|
||||
{
|
||||
model: {
|
||||
parameters: { param2: 'value2' },
|
||||
settings: { setting2: 'value2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
it('should fetch and set threads data', async () => {
|
||||
// Mock Jotai hooks
|
||||
const mockSetThreadStates = jest.fn()
|
||||
const mockSetThreads = jest.fn()
|
||||
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||
const mockSetThreadDataReady = jest.fn()
|
||||
|
||||
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||
switch (atom) {
|
||||
case threadStatesAtom:
|
||||
return mockSetThreadStates
|
||||
case threadsAtom:
|
||||
return mockSetThreads
|
||||
case threadModelParamsAtom:
|
||||
return mockSetThreadModelRuntimeParams
|
||||
case threadDataReadyAtom:
|
||||
return mockSetThreadDataReady
|
||||
default:
|
||||
return jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
// Mock extensionManager
|
||||
const mockGetThreads = jest.fn().mockResolvedValue(mockThreads)
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
getThreads: mockGetThreads,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useThreads())
|
||||
|
||||
await act(async () => {
|
||||
// Wait for useEffect to complete
|
||||
})
|
||||
|
||||
// Assertions
|
||||
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||
ExtensionTypeEnum.Conversational
|
||||
)
|
||||
expect(mockGetThreads).toHaveBeenCalled()
|
||||
|
||||
expect(mockSetThreadStates).toHaveBeenCalledWith({
|
||||
thread1: {
|
||||
hasMore: false,
|
||||
waitingForResponse: false,
|
||||
lastMessage: 'Hello',
|
||||
},
|
||||
thread2: {
|
||||
hasMore: false,
|
||||
waitingForResponse: false,
|
||||
lastMessage: 'Hi there',
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockSetThreads).toHaveBeenCalledWith(mockThreads)
|
||||
|
||||
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({
|
||||
thread1: { param1: 'value1', setting1: 'value1' },
|
||||
thread2: { param2: 'value2', setting2: 'value2' },
|
||||
})
|
||||
|
||||
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should handle empty threads', async () => {
|
||||
// Mock empty threads
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
getThreads: jest.fn().mockResolvedValue([]),
|
||||
})
|
||||
|
||||
const mockSetThreadStates = jest.fn()
|
||||
const mockSetThreads = jest.fn()
|
||||
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||
const mockSetThreadDataReady = jest.fn()
|
||||
|
||||
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||
switch (atom) {
|
||||
case threadStatesAtom:
|
||||
return mockSetThreadStates
|
||||
case threadsAtom:
|
||||
return mockSetThreads
|
||||
case threadModelParamsAtom:
|
||||
return mockSetThreadModelRuntimeParams
|
||||
case threadDataReadyAtom:
|
||||
return mockSetThreadDataReady
|
||||
default:
|
||||
return jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useThreads())
|
||||
|
||||
await act(async () => {
|
||||
// Wait for useEffect to complete
|
||||
})
|
||||
|
||||
expect(mockSetThreadStates).toHaveBeenCalledWith({})
|
||||
expect(mockSetThreads).toHaveBeenCalledWith([])
|
||||
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
|
||||
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should handle missing ConversationalExtension', async () => {
|
||||
// Mock missing ConversationalExtension
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue(null)
|
||||
|
||||
const mockSetThreadStates = jest.fn()
|
||||
const mockSetThreads = jest.fn()
|
||||
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||
const mockSetThreadDataReady = jest.fn()
|
||||
|
||||
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||
switch (atom) {
|
||||
case threadStatesAtom:
|
||||
return mockSetThreadStates
|
||||
case threadsAtom:
|
||||
return mockSetThreads
|
||||
case threadModelParamsAtom:
|
||||
return mockSetThreadModelRuntimeParams
|
||||
case threadDataReadyAtom:
|
||||
return mockSetThreadDataReady
|
||||
default:
|
||||
return jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useThreads())
|
||||
|
||||
await act(async () => {
|
||||
// Wait for useEffect to complete
|
||||
})
|
||||
|
||||
expect(mockSetThreadStates).toHaveBeenCalledWith({})
|
||||
expect(mockSetThreads).toHaveBeenCalledWith([])
|
||||
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
|
||||
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
@ -11,12 +11,12 @@ import { useSetAtom } from 'jotai'
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import {
|
||||
ModelParams,
|
||||
threadDataReadyAtom,
|
||||
threadModelParamsAtom,
|
||||
threadStatesAtom,
|
||||
threadsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
import { ModelParams } from '@/types/model'
|
||||
|
||||
const useThreads = () => {
|
||||
const setThreadStates = useSetAtom(threadStatesAtom)
|
||||
|
||||
@ -18,10 +18,10 @@ import {
|
||||
import { extensionManager } from '@/extension'
|
||||
import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
import {
|
||||
ModelParams,
|
||||
getActiveThreadModelParamsAtom,
|
||||
setThreadModelParamsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
import { ModelParams } from '@/types/model'
|
||||
|
||||
export type UpdateModelParameter = {
|
||||
params?: ModelParams
|
||||
|
||||
4
web/types/model.d.ts
vendored
Normal file
4
web/types/model.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* ModelParams types
|
||||
*/
|
||||
export type ModelParams = ModelRuntimeParams | ModelSettingParams
|
||||
@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { ModelRuntimeParams, ModelSettingParams } from '@janhq/core'
|
||||
|
||||
import { ModelParams } from '@/helpers/atoms/Thread.atom'
|
||||
import { ModelParams } from '@/types/model'
|
||||
|
||||
/**
|
||||
* Validation rules for model parameters
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user