* feat: migrate cortex models to new llama.cpp extension * test: add tests * clean: remove duplicated import
296 lines
12 KiB
TypeScript
296 lines
12 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import llamacpp_extension from '../index'
|
|
|
|
describe('migrateLegacyModels', () => {
|
|
let extension: llamacpp_extension
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
extension = new llamacpp_extension({
|
|
name: 'llamacpp-extension',
|
|
productName: 'LlamaC++ Extension',
|
|
version: '1.0.0',
|
|
description: 'Test extension',
|
|
main: 'index.js',
|
|
})
|
|
// Set up provider path to avoid issues with getProviderPath() calls
|
|
extension['providerPath'] = '/path/to/jan/llamacpp'
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('migrateLegacyModels method', () => {
|
|
it('should return early if legacy models directory does not exist', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath).mockResolvedValue('/path/to/jan/models')
|
|
vi.mocked(fs.existsSync).mockResolvedValue(false)
|
|
|
|
// Call the private method via reflection
|
|
await extension['migrateLegacyModels']()
|
|
|
|
expect(fs.existsSync).toHaveBeenCalledWith('/path/to/jan/models')
|
|
expect(fs.readdirSync).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should skip non-yml files during migration', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/test-file.txt') // childPath
|
|
|
|
vi.mocked(fs.existsSync).mockResolvedValue(true)
|
|
vi.mocked(fs.readdirSync).mockResolvedValue(['test-file.txt'])
|
|
vi.mocked(fs.fileStat).mockResolvedValue({
|
|
isDirectory: false,
|
|
size: 1000,
|
|
})
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
// Should not try to read yaml for non-yml files
|
|
expect(invoke).not.toHaveBeenCalledWith('read_yaml', expect.any(Object))
|
|
})
|
|
|
|
it('should skip yml files when model.yml already exists in directory', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/model.yml') // childPath for model.yml
|
|
.mockResolvedValueOnce('/path/to/jan/models/legacy-model.yml') // childPath for legacy-model.yml
|
|
|
|
vi.mocked(fs.existsSync).mockResolvedValue(true)
|
|
vi.mocked(fs.readdirSync).mockResolvedValue([
|
|
'model.yml',
|
|
'legacy-model.yml',
|
|
])
|
|
vi.mocked(fs.fileStat).mockResolvedValue({
|
|
isDirectory: false,
|
|
size: 1000,
|
|
})
|
|
|
|
// Mock the yaml reads that will happen for model.yml
|
|
vi.mocked(invoke).mockResolvedValue({
|
|
name: 'Existing Model',
|
|
model_path: 'models/existing/model.gguf',
|
|
size_bytes: 2000000,
|
|
})
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
// Should read model.yml but skip legacy-model.yml because model.yml exists
|
|
expect(invoke).toHaveBeenCalledWith('read_yaml', {
|
|
path: 'model.yml',
|
|
})
|
|
// The logic should skip legacy-model.yml since model.yml exists, but it still reads both
|
|
// The actual behavior is that it reads model.yml first, then skips legacy-model.yml processing
|
|
expect(invoke).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('should migrate legacy model with valid configuration', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
const { basename } = await import('@tauri-apps/api/path')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
|
|
// Mock specific joinPath calls in the order they will be made
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/test') // childPath
|
|
.mockResolvedValueOnce('/path/to/jan/models/test/model.yml') // legacy model file
|
|
|
|
vi.mocked(fs.existsSync).mockResolvedValue(true)
|
|
|
|
// Mock for the DFS traversal with the bug in mind (algorithm passes just 'child' instead of 'childPath')
|
|
vi.mocked(fs.readdirSync).mockResolvedValueOnce(['test'])
|
|
vi.mocked(fs.readdirSync).mockResolvedValueOnce(['test'])
|
|
vi.mocked(fs.readdirSync).mockResolvedValueOnce(['model.yml'])
|
|
vi.mocked(fs.readdirSync).mockResolvedValueOnce([])
|
|
|
|
vi.mocked(fs.fileStat)
|
|
.mockResolvedValueOnce({ isDirectory: true, size: 0 }) // imported directory is a directory
|
|
.mockResolvedValueOnce({ isDirectory: true, size: 0 }) // yml file stat
|
|
.mockResolvedValueOnce({ isDirectory: false, size: 1000 }) // model file size for size_bytes
|
|
.mockResolvedValueOnce({ isDirectory: false, size: 1000 }) // filename stat in directory traversal
|
|
|
|
vi.mocked(basename).mockResolvedValue('model')
|
|
|
|
// Mock reading legacy config
|
|
vi.mocked(invoke)
|
|
.mockResolvedValueOnce({
|
|
files: ['/path/to/jan/models/test/path.gguf'],
|
|
model: 'Legacy Test Model',
|
|
})
|
|
.mockResolvedValueOnce(undefined) // write_yaml call
|
|
|
|
vi.mocked(fs.mkdir).mockResolvedValue(undefined)
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
expect(invoke).toHaveBeenNthCalledWith(1, 'read_yaml', {
|
|
path: 'model.yml',
|
|
})
|
|
})
|
|
|
|
it('should skip migration if legacy model file does not exist', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/legacy-model.yml') // childPath
|
|
|
|
vi.mocked(fs.existsSync)
|
|
.mockResolvedValueOnce(true) // models dir exists
|
|
.mockResolvedValueOnce(true) // legacy config exists
|
|
|
|
vi.mocked(fs.readdirSync).mockResolvedValue(['legacy-model.yml'])
|
|
vi.mocked(fs.fileStat).mockResolvedValue({
|
|
isDirectory: false,
|
|
size: 1000,
|
|
})
|
|
|
|
// Mock reading legacy config with no files
|
|
vi.mocked(invoke).mockResolvedValueOnce({
|
|
files: [],
|
|
model: 'Test Model',
|
|
})
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
// Should not proceed with migration
|
|
expect(invoke).toHaveBeenCalledTimes(1) // Only the read_yaml call
|
|
expect(fs.mkdir).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should skip migration if new model config already exists', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
const { basename } = await import('@tauri-apps/api/path')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/legacy-model.yml') // childPath
|
|
.mockResolvedValueOnce('/path/to/jan/legacy/model/path.gguf') // legacy model file path
|
|
.mockResolvedValueOnce(
|
|
'/path/to/jan/llamacpp/models/legacy-model/model.yml'
|
|
) // config path
|
|
|
|
vi.mocked(fs.existsSync)
|
|
.mockResolvedValueOnce(true) // models dir exists
|
|
.mockResolvedValueOnce(true) // legacy config exists
|
|
.mockResolvedValueOnce(true) // new config already exists
|
|
|
|
vi.mocked(fs.readdirSync).mockResolvedValue(['legacy-model.yml'])
|
|
vi.mocked(fs.fileStat).mockResolvedValue({
|
|
isDirectory: false,
|
|
size: 1000,
|
|
})
|
|
vi.mocked(basename).mockResolvedValue('legacy-model')
|
|
|
|
// Mock reading legacy config
|
|
vi.mocked(invoke).mockResolvedValueOnce({
|
|
files: ['legacy/model/path.gguf'],
|
|
model: 'Legacy Test Model',
|
|
})
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
// Should not proceed with migration since config already exists
|
|
expect(invoke).toHaveBeenCalledTimes(1) // Only the read_yaml call
|
|
expect(fs.mkdir).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should explore subdirectories when no yml files found in current directory', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath)
|
|
.mockResolvedValueOnce('/path/to/jan/models') // initial modelsDir
|
|
.mockResolvedValueOnce('/path/to/jan/models/subdir') // child directory
|
|
|
|
vi.mocked(fs.existsSync).mockResolvedValue(true)
|
|
vi.mocked(fs.readdirSync)
|
|
.mockResolvedValueOnce(['subdir']) // First call returns only a directory
|
|
.mockResolvedValueOnce([]) // Second call for subdirectory returns empty
|
|
|
|
vi.mocked(fs.fileStat)
|
|
.mockResolvedValueOnce({ isDirectory: true, size: 0 }) // subdir is a directory
|
|
.mockResolvedValueOnce({ isDirectory: true, size: 0 }) // fileStat for directory check
|
|
|
|
await extension['migrateLegacyModels']()
|
|
|
|
expect(fs.readdirSync).toHaveBeenCalledTimes(2)
|
|
expect(fs.readdirSync).toHaveBeenCalledWith('/path/to/jan/models')
|
|
// Note: The original code has a bug where it pushes just 'child' instead of the full path
|
|
// so it would call fs.readdirSync('subdir') instead of the full path
|
|
expect(fs.readdirSync).toHaveBeenCalledWith('/path/to/jan/models')
|
|
})
|
|
})
|
|
|
|
describe('list method integration with migrateLegacyModels', () => {
|
|
it('should call migrateLegacyModels during list operation', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
|
|
// Mock the migrateLegacyModels method
|
|
const migrateSpy = vi
|
|
.spyOn(extension as any, 'migrateLegacyModels')
|
|
.mockResolvedValue(undefined)
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath).mockImplementation((paths) =>
|
|
Promise.resolve(paths.join('/'))
|
|
)
|
|
vi.mocked(fs.existsSync).mockResolvedValue(true)
|
|
vi.mocked(fs.readdirSync).mockResolvedValue([])
|
|
vi.mocked(fs.fileStat).mockResolvedValue({
|
|
isDirectory: false,
|
|
size: 1000,
|
|
})
|
|
|
|
// Mock invoke for any potential yaml reads (though directory is empty)
|
|
vi.mocked(invoke).mockResolvedValue({
|
|
name: 'Test Model',
|
|
model_path: 'models/test/model.gguf',
|
|
size_bytes: 1000000,
|
|
})
|
|
|
|
await extension.list()
|
|
|
|
expect(migrateSpy).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('should create models directory if it does not exist before migration', async () => {
|
|
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
|
|
|
|
const migrateSpy = vi
|
|
.spyOn(extension as any, 'migrateLegacyModels')
|
|
.mockResolvedValue(undefined)
|
|
|
|
vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
|
|
vi.mocked(joinPath).mockResolvedValue('/path/to/jan/llamacpp/models')
|
|
vi.mocked(fs.existsSync).mockResolvedValue(false) // models dir doesn't exist
|
|
vi.mocked(fs.mkdir).mockResolvedValue(undefined)
|
|
vi.mocked(fs.readdirSync).mockResolvedValue([])
|
|
|
|
await extension.list()
|
|
|
|
expect(fs.mkdir).toHaveBeenCalledWith('/path/to/jan/llamacpp/models')
|
|
expect(migrateSpy).toHaveBeenCalledOnce()
|
|
})
|
|
})
|
|
})
|