* fix: mismatch between model json and path * chore: revert preserve model settings * test: add tests
565 lines
17 KiB
TypeScript
565 lines
17 KiB
TypeScript
const readDirSyncMock = jest.fn()
|
|
const existMock = jest.fn()
|
|
const readFileSyncMock = jest.fn()
|
|
|
|
jest.mock('@janhq/core', () => ({
|
|
...jest.requireActual('@janhq/core/node'),
|
|
fs: {
|
|
existsSync: existMock,
|
|
readdirSync: readDirSyncMock,
|
|
readFileSync: readFileSyncMock,
|
|
fileStat: () => ({
|
|
isDirectory: false,
|
|
}),
|
|
},
|
|
dirName: jest.fn(),
|
|
joinPath: (paths) => paths.join('/'),
|
|
ModelExtension: jest.fn(),
|
|
}))
|
|
|
|
import JanModelExtension from '.'
|
|
import { fs, dirName } from '@janhq/core'
|
|
|
|
describe('JanModelExtension', () => {
|
|
let sut: JanModelExtension
|
|
|
|
beforeAll(() => {
|
|
// @ts-ignore
|
|
sut = new JanModelExtension()
|
|
})
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
describe('getConfiguredModels', () => {
|
|
describe("when there's no models are pre-populated", () => {
|
|
it('should return empty array', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = []
|
|
existMock.mockReturnValue(true)
|
|
readDirSyncMock.mockReturnValue([])
|
|
|
|
const result = await sut.getConfiguredModels()
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe("when there's are pre-populated models - all flattened", () => {
|
|
it('returns configured models data - flatten folder - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2']
|
|
else return ['model.json']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getConfiguredModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model2/model.json',
|
|
id: '2',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("when there's are pre-populated models - there are nested folders", () => {
|
|
it('returns configured models data - flatten folder - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2/model2-1']
|
|
else return ['model.json']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else if (path.includes('model2/model2-1'))
|
|
return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getConfiguredModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model2/model2-1/model.json',
|
|
id: '2',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getDownloadedModels', () => {
|
|
describe('no models downloaded', () => {
|
|
it('should return empty array', async () => {
|
|
// Mock downloaded models data
|
|
const downloadedModels = []
|
|
existMock.mockReturnValue(true)
|
|
readDirSyncMock.mockReturnValue([])
|
|
|
|
const result = await sut.getDownloadedModels()
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
describe('only one model is downloaded', () => {
|
|
describe('flatten folder', () => {
|
|
it('returns downloaded models - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2']
|
|
else if (path === 'file://models/model1')
|
|
return ['model.json', 'test.gguf']
|
|
else return ['model.json']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getDownloadedModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('all models are downloaded', () => {
|
|
describe('nested folders', () => {
|
|
it('returns downloaded models - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2/model2-1']
|
|
else return ['model.json', 'test.gguf']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getDownloadedModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model2/model2-1/model.json',
|
|
id: '2',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('all models are downloaded with uppercased GGUF files', () => {
|
|
it('returns downloaded models - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2/model2-1']
|
|
else if (path === 'file://models/model1')
|
|
return ['model.json', 'test.GGUF']
|
|
else return ['model.json', 'test.gguf']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getDownloadedModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model2/model2-1/model.json',
|
|
id: '2',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('all models are downloaded - GGUF & Tensort RT', () => {
|
|
it('returns downloaded models - with correct file_path and model id', async () => {
|
|
// Mock configured models data
|
|
const configuredModels = [
|
|
{
|
|
id: '1',
|
|
name: 'Model 1',
|
|
version: '1.0.0',
|
|
description: 'Model 1 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model1',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
created: new Date(),
|
|
updated: new Date(),
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
{
|
|
id: '2',
|
|
name: 'Model 2',
|
|
version: '2.0.0',
|
|
description: 'Model 2 description',
|
|
object: {
|
|
type: 'model',
|
|
uri: 'http://localhost:5000/models/model2',
|
|
},
|
|
format: 'onnx',
|
|
sources: [],
|
|
parameters: {},
|
|
settings: {},
|
|
metadata: {},
|
|
engine: 'test',
|
|
} as any,
|
|
]
|
|
existMock.mockReturnValue(true)
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
if (path === 'file://models') return ['model1', 'model2/model2-1']
|
|
else if (path === 'file://models/model1')
|
|
return ['model.json', 'test.gguf']
|
|
else return ['model.json', 'test.engine']
|
|
})
|
|
|
|
readFileSyncMock.mockImplementation((path) => {
|
|
if (path.includes('model1'))
|
|
return JSON.stringify(configuredModels[0])
|
|
else return JSON.stringify(configuredModels[1])
|
|
})
|
|
|
|
const result = await sut.getDownloadedModels()
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model1/model.json',
|
|
id: '1',
|
|
}),
|
|
expect.objectContaining({
|
|
file_path: 'file://models/model2/model2-1/model.json',
|
|
id: '2',
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteModel', () => {
|
|
describe('model is a GGUF model', () => {
|
|
it('should delete the GGUF file', async () => {
|
|
fs.unlinkSync = jest.fn()
|
|
const dirMock = dirName as jest.Mock
|
|
dirMock.mockReturnValue('file://models/model1')
|
|
|
|
fs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({}))
|
|
|
|
readDirSyncMock.mockImplementation((path) => {
|
|
return ['model.json', 'test.gguf']
|
|
})
|
|
|
|
existMock.mockReturnValue(true)
|
|
|
|
await sut.deleteModel({
|
|
file_path: 'file://models/model1/model.json',
|
|
} as any)
|
|
|
|
expect(fs.unlinkSync).toHaveBeenCalledWith(
|
|
'file://models/model1/test.gguf'
|
|
)
|
|
})
|
|
|
|
it('no gguf file presented', async () => {
|
|
fs.unlinkSync = jest.fn()
|
|
const dirMock = dirName as jest.Mock
|
|
dirMock.mockReturnValue('file://models/model1')
|
|
|
|
fs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({}))
|
|
|
|
readDirSyncMock.mockReturnValue(['model.json'])
|
|
|
|
existMock.mockReturnValue(true)
|
|
|
|
await sut.deleteModel({
|
|
file_path: 'file://models/model1/model.json',
|
|
} as any)
|
|
|
|
expect(fs.unlinkSync).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
it('delete an imported model', async () => {
|
|
fs.rm = jest.fn()
|
|
const dirMock = dirName as jest.Mock
|
|
dirMock.mockReturnValue('file://models/model1')
|
|
|
|
readDirSyncMock.mockReturnValue(['model.json', 'test.gguf'])
|
|
|
|
// MARK: This is a tricky logic implement?
|
|
// I will just add test for now but will align on the legacy implementation
|
|
fs.readFileSync = jest.fn().mockReturnValue(
|
|
JSON.stringify({
|
|
metadata: {
|
|
author: 'user',
|
|
},
|
|
})
|
|
)
|
|
|
|
existMock.mockReturnValue(true)
|
|
|
|
await sut.deleteModel({
|
|
file_path: 'file://models/model1/model.json',
|
|
} as any)
|
|
|
|
expect(fs.rm).toHaveBeenCalledWith('file://models/model1')
|
|
})
|
|
|
|
it('delete tensorrt-models', async () => {
|
|
fs.rm = jest.fn()
|
|
const dirMock = dirName as jest.Mock
|
|
dirMock.mockReturnValue('file://models/model1')
|
|
|
|
readDirSyncMock.mockReturnValue(['model.json', 'test.engine'])
|
|
|
|
fs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({}))
|
|
|
|
existMock.mockReturnValue(true)
|
|
|
|
await sut.deleteModel({
|
|
file_path: 'file://models/model1/model.json',
|
|
} as any)
|
|
|
|
expect(fs.unlinkSync).toHaveBeenCalledWith('file://models/model1/test.engine')
|
|
})
|
|
})
|
|
})
|
|
})
|