diff --git a/packages/backend/src/managers/applicationManager.spec.ts b/packages/backend/src/managers/applicationManager.spec.ts index 04321edd4..fe835be8a 100644 --- a/packages/backend/src/managers/applicationManager.spec.ts +++ b/packages/backend/src/managers/applicationManager.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, vi } from 'vitest'; +import { type MockInstance, describe, expect, test, vi, beforeEach } from 'vitest'; import { ApplicationManager } from './applicationManager'; import type { RecipeStatusRegistry } from '../registries/RecipeStatusRegistry'; import type { ExtensionContext } from '@podman-desktop/api'; @@ -8,10 +8,13 @@ import fs, { Stats, type Dirent } from 'fs'; import path from 'path'; import type { Recipe } from '@shared/src/models/IRecipe'; import type { ModelInfo } from '@shared/src/models/IModelInfo'; +import type { LocalModelInfo } from '@shared/src/models/ILocalModelInfo'; +import type { RecipeStatusUtils } from '../utils/recipeStatusUtils'; const mocks = vi.hoisted(() => { return { parseYamlMock: vi.fn(), + builImageMock: vi.fn(), }; }); @@ -19,6 +22,16 @@ vi.mock('../models/AIConfig', () => ({ parseYaml: mocks.parseYamlMock, })); +vi.mock('@podman-desktop/api', () => ({ + containerEngine: { + buildImage: mocks.builImageMock, + }, +})); + +beforeEach(() => { + vi.resetAllMocks(); +}); + test('appUserDirectory should be under home directory', () => { vi.spyOn(os, 'homedir').mockReturnValue('/home/user'); const manager = new ApplicationManager({} as GitManager, {} as RecipeStatusRegistry, {} as ExtensionContext); @@ -71,76 +84,175 @@ test('getLocalModels should return models in local directory', () => { ]); }); -test('pullApplication should clone repository and call downloadModelMain', async () => { +describe('pullApplication', () => { + interface mockForPullApplicationOptions { + recipeFolderExists: boolean; + } + const setStatusMock = vi.fn(); const cloneRepositoryMock = vi.fn(); - vi.spyOn(os, 'homedir').mockReturnValue('/home/user'); - vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); - vi.spyOn(fs, 'existsSync').mockImplementation((path: string) => { - if (path.endsWith('recipe1')) { + let manager: ApplicationManager; + let getLocalModelsSpy: MockInstance<[], LocalModelInfo[]>; + let downloadModelMainSpy: MockInstance< + [modelId: string, url: string, taskUtil: RecipeStatusUtils, destFileName?: string], + Promise + >; + + function mockForPullApplication(options: mockForPullApplicationOptions) { + vi.spyOn(os, 'homedir').mockReturnValue('/home/user'); + vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined); + vi.spyOn(fs, 'existsSync').mockImplementation((path: string) => { + if (path.endsWith('recipe1')) { + return options.recipeFolderExists; + } else if (path.endsWith('ai-studio.yaml')) { + return true; + } else if (path.endsWith('contextdir1')) { + return true; + } return false; - } else if (path.endsWith('ai-studio.yaml')) { - return true; - } - return false; - }); - vi.spyOn(fs, 'statSync').mockImplementation((path: string) => { - if (path.endsWith('recipe1')) { - const stat = new Stats(); - stat.isDirectory = () => true; - return stat; - } else if (path.endsWith('ai-studio.yaml')) { - const stat = new Stats(); - stat.isDirectory = () => false; - return stat; + }); + vi.spyOn(fs, 'statSync').mockImplementation((path: string) => { + if (path.endsWith('recipe1')) { + const stat = new Stats(); + stat.isDirectory = () => true; + return stat; + } else if (path.endsWith('ai-studio.yaml')) { + const stat = new Stats(); + stat.isDirectory = () => false; + return stat; + } + }); + vi.spyOn(fs, 'readFileSync').mockImplementation((_path: string) => { + return ''; + }); + mocks.parseYamlMock.mockReturnValue({ + application: { + containers: [ + { + name: 'container1', + contextdir: 'contextdir1', + containerfile: 'Containerfile', + }, + ], + }, + }); + mocks.builImageMock.mockResolvedValue(undefined); + + manager = new ApplicationManager( + { + cloneRepository: cloneRepositoryMock, + } as unknown as GitManager, + { + setStatus: setStatusMock, + } as unknown as RecipeStatusRegistry, + {} as ExtensionContext, + ); + + getLocalModelsSpy = vi.spyOn(manager, 'getLocalModels'); + downloadModelMainSpy = vi.spyOn(manager, 'downloadModelMain'); + downloadModelMainSpy.mockResolvedValue(''); + } + + test('pullApplication should clone repository and call downloadModelMain and buildImage', async () => { + mockForPullApplication({ + recipeFolderExists: false, + }); + getLocalModelsSpy.mockReturnValue([]); + + const recipe: Recipe = { + id: 'recipe1', + name: 'Recipe 1', + categories: [], + description: '', + readme: '', + repository: 'repo', + }; + const model: ModelInfo = { + id: 'model1', + description: '', + hw: '', + license: '', + name: 'Model 1', + popularity: 1, + registry: '', + url: '', + }; + + await manager.pullApplication(recipe, model); + if (process.platform === 'win32') { + expect(cloneRepositoryMock).toHaveBeenNthCalledWith( + 1, + 'repo', + '\\home\\user\\podman-desktop\\ai-studio\\recipe1', + ); + } else { + expect(cloneRepositoryMock).toHaveBeenNthCalledWith(1, 'repo', '/home/user/podman-desktop/ai-studio/recipe1'); } + expect(downloadModelMainSpy).toHaveBeenCalledOnce(); + expect(mocks.builImageMock).toHaveBeenCalledOnce(); }); - vi.spyOn(fs, 'readFileSync').mockImplementation((_path: string) => { - return ''; + + test('pullApplication should not clone repository if folder already exists locally', async () => { + mockForPullApplication({ + recipeFolderExists: true, + }); + getLocalModelsSpy.mockReturnValue([]); + + const recipe: Recipe = { + id: 'recipe1', + name: 'Recipe 1', + categories: [], + description: '', + readme: '', + repository: 'repo', + }; + const model: ModelInfo = { + id: 'model1', + description: '', + hw: '', + license: '', + name: 'Model 1', + popularity: 1, + registry: '', + url: '', + }; + + await manager.pullApplication(recipe, model); + expect(cloneRepositoryMock).not.toHaveBeenCalled(); }); - mocks.parseYamlMock.mockReturnValue({ - application: { - containers: [], - }, + + test('pullApplication should not download model if already on disk', async () => { + mockForPullApplication({ + recipeFolderExists: true, + }); + getLocalModelsSpy.mockReturnValue([ + { + id: 'model1', + file: 'model1.file', + }, + ]); + + const recipe: Recipe = { + id: 'recipe1', + name: 'Recipe 1', + categories: [], + description: '', + readme: '', + repository: 'repo', + }; + const model: ModelInfo = { + id: 'model1', + description: '', + hw: '', + license: '', + name: 'Model 1', + popularity: 1, + registry: '', + url: '', + }; + + await manager.pullApplication(recipe, model); + expect(cloneRepositoryMock).not.toHaveBeenCalled(); + expect(downloadModelMainSpy).not.toHaveBeenCalled(); }); - const manager = new ApplicationManager( - { - cloneRepository: cloneRepositoryMock, - } as unknown as GitManager, - { - setStatus: setStatusMock, - } as unknown as RecipeStatusRegistry, - {} as ExtensionContext, - ); - - const getLocalModelsSpy = vi.spyOn(manager, 'getLocalModels'); - getLocalModelsSpy.mockReturnValue([]); - const downloadModelMainSpy = vi.spyOn(manager, 'downloadModelMain'); - downloadModelMainSpy.mockResolvedValue(''); - - const recipe: Recipe = { - id: 'recipe1', - name: 'Recipe 1', - categories: [], - description: '', - readme: '', - repository: 'repo', - }; - const model: ModelInfo = { - id: 'model1', - description: '', - hw: '', - license: '', - name: 'Model 1', - popularity: 1, - registry: '', - url: '', - }; - await manager.pullApplication(recipe, model); - if (process.platform === 'win32') { - expect(cloneRepositoryMock).toHaveBeenNthCalledWith(1, 'repo', '\\home\\user\\podman-desktop\\ai-studio\\recipe1'); - } else { - expect(cloneRepositoryMock).toHaveBeenNthCalledWith(1, 'repo', '/home/user/podman-desktop/ai-studio/recipe1'); - } - expect(downloadModelMainSpy).toHaveBeenCalledOnce(); }); diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 0c675e04b..d86d61da3 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -23,9 +23,13 @@ const PACKAGE_ROOT = __dirname; const config = { test: { - include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', '../shared/**/*.{test,spec}.?(c|m)[jt]s?(x)'] - }, - resolve: { + include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', '../shared/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + coverage: { + provider: 'v8', + reporter: ['lcov', 'text'], + }, +}, +resolve: { alias: { '@podman-desktop/api': path.resolve(__dirname, '__mocks__/@podman-desktop/api.js'), '/@/': join(PACKAGE_ROOT, 'src') + '/',