diff --git a/src/config/comfyServerConfig.ts b/src/config/comfyServerConfig.ts new file mode 100644 index 00000000..02cfc01b --- /dev/null +++ b/src/config/comfyServerConfig.ts @@ -0,0 +1,234 @@ +import * as fsPromises from 'node:fs/promises'; +import log from 'electron-log/main'; +import yaml from 'yaml'; +import path from 'node:path'; +import { app } from 'electron'; + +const knownModelKeys = [ + 'checkpoints', + 'classifiers', + 'clip', + 'clip_vision', + 'configs', + 'controlnet', + 'diffusers', + 'diffusion_models', + 'embeddings', + 'gligen', + 'hypernetworks', + 'loras', + 'photomaker', + 'style_models', + 'unet', + 'upscale_models', + 'vae', + 'vae_approx', + // TODO(robinhuang): Remove when we have a better way to specify base model paths. + 'animatediff_models', + 'animatediff_motion_lora', + 'animatediff_video_formats', + 'ipadapter', + 'liveportrait', + 'insightface', + 'layerstyle', + 'LLM', + 'Joy_caption', + 'sams', + 'blip', + 'CogVideo', + 'xlabs', + 'instantid', +] as const; + +type ModelPaths = Record; + +/** + * The ComfyServerConfig class is used to manage the configuration for the ComfyUI server. + */ +export class ComfyServerConfig { + // The name of the default config file. + static readonly COMFYUI_DEFAULT_CONFIG_NAME = 'extra_model_paths.yaml'; + // The path to the extra_models_config.yaml file used by the Electron desktop app. + static readonly EXTRA_MODEL_CONFIG_PATH = 'extra_models_config.yaml'; + + private static readonly commonPaths = { + ...this.getBaseModelPathsFromRepoPath(''), + custom_nodes: 'custom_nodes/', + }; + private static readonly configTemplates: Record = { + win32: { + is_default: 'true', + ...this.commonPaths, + }, + darwin: { + is_default: 'true', + ...this.commonPaths, + }, + linux: { + is_default: 'true', + ...this.commonPaths, + }, + } as const; + + /** + * The path to the extra_models_config.yaml file. The config file is used for ComfyUI core to determine search paths + * for models and custom nodes. + */ + public static get configPath(): string { + return path.join(app.getPath('userData'), ComfyServerConfig.EXTRA_MODEL_CONFIG_PATH); + } + + /** + * Get the base config for the current operating system. + */ + static getBaseConfig(): ModelPaths | null { + for (const [operatingSystem, modelPathConfig] of Object.entries(this.configTemplates)) { + if (operatingSystem === process.platform) { + return modelPathConfig; + } + } + return null; + } + /** + * Generate the content for the extra_model_paths.yaml file. + */ + static generateConfigFileContent(modelPathConfigs: Record): string { + const modelConfigYaml = yaml.stringify(modelPathConfigs, { lineWidth: -1 }); + return `# ComfyUI extra_model_paths.yaml for ${process.platform}\n${modelConfigYaml}`; + } + + static mergeConfig(baseConfig: ModelPaths, customConfig: ModelPaths): ModelPaths { + const mergedConfig: ModelPaths = { ...baseConfig }; + + for (const [key, customPath] of Object.entries(customConfig)) { + if (key in baseConfig) { + // Concatenate paths if key exists in both configs + // Order here matters, as ComfyUI searches for models in the order they are listed. + mergedConfig[key] = baseConfig[key] + '\n' + customPath; + } else { + // Use custom path directly if key only exists in custom config + mergedConfig[key] = customPath; + } + } + + return mergedConfig; + } + + static async writeConfigFile(configFilePath: string, content: string): Promise { + try { + await fsPromises.writeFile(configFilePath, content, 'utf8'); + log.info(`Created extra_model_paths.yaml at ${configFilePath}`); + return true; + } catch (error) { + log.error('Error writing config file:', error); + return false; + } + } + + public static async readConfigFile(configPath: string): Promise | null> { + try { + const fileContent = await fsPromises.readFile(configPath, 'utf8'); + const config = yaml.parse(fileContent); + return config; + } catch (error) { + log.error(`Error reading config file ${configPath}:`, error); + return null; + } + } + + public static async getConfigFromRepoPath(repoPath: string): Promise> { + const configPath = path.join(repoPath, ComfyServerConfig.COMFYUI_DEFAULT_CONFIG_NAME); + const config = (await this.readConfigFile(configPath)) ?? {}; + return config; + } + + public static getBaseModelPathsFromRepoPath(repoPath: string): ModelPaths { + return knownModelKeys.reduce((acc, key) => { + acc[key] = path.join(repoPath, 'models', key) + path.sep; + return acc; + }, {} as ModelPaths); + } + + /** + * Create the extra_model_paths.yaml file in the given destination path with the given custom config. + * @param destinationPath - The path to the destination file. + * @param customConfig - The custom config to merge with the base config. + * @param extraConfigs - The extra configs such as paths from A1111. + */ + public static async createConfigFile( + destinationPath: string, + customConfig: ModelPaths, + extraConfigs: Record + ): Promise { + log.info(`Creating model config files in ${destinationPath}`); + try { + const baseConfig = this.getBaseConfig(); + if (!baseConfig) { + log.error('No base config found'); + return false; + } + const comfyuiConfig = this.mergeConfig(baseConfig, customConfig); + const configContent = this.generateConfigFileContent({ + ...extraConfigs, + comfyui: comfyuiConfig, + }); + return await this.writeConfigFile(destinationPath, configContent); + } catch (error) { + log.error('Error creating model config files:', error); + return false; + } + } + + public static async readBasePathFromConfig(configPath: string): Promise { + try { + const fileContent = await fsPromises.readFile(configPath, 'utf8'); + const config = yaml.parse(fileContent); + + if (config?.comfyui?.base_path) { + return config.comfyui.base_path; + } + + log.warn(`No base_path found in ${configPath}`); + return null; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + log.info(`Config file not found at ${configPath}`); + } else { + log.error(`Error reading config file ${configPath}:`, error); + } + return null; + } + } + + /** + * Get the config for the migration source (Existing ComfyUI instance). + * @param migrationSource - The path to the migration source. + * @param migrationItemIds - The item ids to migrate. + */ + public static async getMigrationConfig( + migrationSource?: string, + migrationItemIds: Set = new Set() + ): Promise> { + if (!migrationSource || !migrationItemIds.has('models')) { + return {}; + } + // The yaml file exited in migration source repo. + const migrationServerConfig = await ComfyServerConfig.getConfigFromRepoPath(migrationSource); + + // The model paths in the migration source repo. + const migrationComfyConfig = migrationSource + ? ComfyServerConfig.getBaseModelPathsFromRepoPath(migrationSource) + : {}; + + // The overall paths to add to the config file. + const comfyuiConfig = ComfyServerConfig.mergeConfig(migrationServerConfig['comfyui'] ?? {}, migrationComfyConfig); + // Do not migrate custom nodes as we currently don't have a way to install their dependencies. + if ('custom_nodes' in comfyuiConfig) { + delete comfyuiConfig['custom_nodes']; + } + return { + ...migrationServerConfig, + comfyui: comfyuiConfig, + }; + } +} diff --git a/src/config/extra_model_config.ts b/src/config/extra_model_config.ts deleted file mode 100644 index c88b5616..00000000 --- a/src/config/extra_model_config.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as fsPromises from 'node:fs/promises'; -import log from 'electron-log/main'; -import { stringify, parse } from 'yaml'; -import path from 'node:path'; -import { app } from 'electron'; - -export const EXTRA_MODEL_CONFIG_PATH = 'extra_models_config.yaml'; - -interface ModelPaths { - comfyui: { - base_path: string; - is_default: boolean; - [key: string]: string | boolean; - }; -} - -const commonPaths = { - is_default: true, - checkpoints: 'models/checkpoints/', - classifiers: 'models/classifiers/', - clip: 'models/clip/', - clip_vision: 'models/clip_vision/', - configs: 'models/configs/', - controlnet: 'models/controlnet/', - diffusers: 'models/diffusers/', - diffusion_models: 'models/diffusion_models/', - embeddings: 'models/embeddings/', - gligen: 'models/gligen/', - hypernetworks: 'models/hypernetworks/', - loras: 'models/loras/', - photomaker: 'models/photomaker/', - style_models: 'models/style_models/', - unet: 'models/unet/', - upscale_models: 'models/upscale_models/', - vae: 'models/vae/', - vae_approx: 'models/vae_approx/', - // TODO(robinhuang): Remove when we have a better way to specify base model paths. - animatediff_models: 'models/animatediff_models/', - animatediff_motion_lora: 'models/animatediff_motion_lora/', - animatediff_video_formats: 'models/animatediff_video_formats/', - ipadapter: 'models/ipadapter/', - liveportrait: 'models/liveportrait/', - insightface: 'models/insightface/', - layerstyle: 'models/layerstyle/', - LLM: 'models/LLM/', - Joy_caption: 'models/Joy_caption/', - sams: 'models/sams/', - blip: 'models/blip/', - CogVideo: 'models/CogVideo/', - xlabs: 'models/xlabs/', - instantid: 'models/instantid/', - // End custom node model directories. - custom_nodes: 'custom_nodes/', -}; - -const configTemplates: Record = { - win32: { - comfyui: { - base_path: '%USERPROFILE%/comfyui-electron', - ...commonPaths, - }, - }, - darwin: { - comfyui: { - base_path: '~/Library/Application Support/ComfyUI', - ...commonPaths, - }, - }, - linux: { - comfyui: { - base_path: '~/.config/ComfyUI', - ...commonPaths, - }, - }, -}; - -export function getModelConfigPath(): string { - return path.join(app.getPath('userData'), EXTRA_MODEL_CONFIG_PATH); -} - -export async function createModelConfigFiles(extraModelConfigPath: string, customBasePath?: string): Promise { - log.info(`Creating model config files in ${extraModelConfigPath} with base path ${customBasePath}`); - try { - for (const [platform, config] of Object.entries(configTemplates)) { - if (platform !== process.platform) { - continue; - } - - log.info(`Creating model config files for ${platform}`); - - // If a custom base path is provided, use it - if (customBasePath) { - config.comfyui.base_path = customBasePath; - } - - const yamlContent = stringify(config, { lineWidth: -1 }); - - // Add a comment at the top of the file - const fileContent = `# ComfyUI extra_model_paths.yaml for ${platform}\n${yamlContent}`; - await fsPromises.writeFile(extraModelConfigPath, fileContent, 'utf8'); - log.info(`Created extra_model_paths.yaml at ${extraModelConfigPath}`); - return true; - } - log.info(`No model config files created for platform ${process.platform}`); - return false; - } catch (error) { - log.error('Error creating model config files:', error); - return false; - } -} - -export async function readBasePathFromConfig(configPath: string): Promise { - try { - const fileContent = await fsPromises.readFile(configPath, 'utf8'); - const config = parse(fileContent); - - if (config && config.comfyui && config.comfyui.base_path) { - return config.comfyui.base_path; - } else { - log.warn(`No base_path found in ${configPath}`); - return null; - } - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - log.info(`Config file not found at ${configPath}`); - } else { - log.error(`Error reading config file ${configPath}:`, error); - } - return null; - } -} diff --git a/src/handlers/pathHandlers.ts b/src/handlers/pathHandlers.ts index 4d374e0c..57272812 100644 --- a/src/handlers/pathHandlers.ts +++ b/src/handlers/pathHandlers.ts @@ -1,7 +1,7 @@ import { app, dialog, ipcMain, shell } from 'electron'; import { IPC_CHANNELS } from '../constants'; import log from 'electron-log/main'; -import { getModelConfigPath } from '../config/extra_model_config'; +import { ComfyServerConfig } from '../config/comfyServerConfig'; import { getBasePath } from '../install/resourcePaths'; import type { SystemPaths } from '../preload'; import fs from 'fs'; @@ -19,7 +19,7 @@ export class PathHandlers { }); ipcMain.handle(IPC_CHANNELS.GET_MODEL_CONFIG_PATH, (): string => { - return getModelConfigPath(); + return ComfyServerConfig.configPath; }); ipcMain.handle(IPC_CHANNELS.GET_BASE_PATH, async (): Promise => { diff --git a/src/install/index.ts b/src/install/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/install/resourcePaths.ts b/src/install/resourcePaths.ts index ea9ef07a..e063a178 100644 --- a/src/install/resourcePaths.ts +++ b/src/install/resourcePaths.ts @@ -1,10 +1,10 @@ import { app } from 'electron'; -import { getModelConfigPath, readBasePathFromConfig } from '../config/extra_model_config'; +import { ComfyServerConfig } from '../config/comfyServerConfig'; import path from 'path'; export async function getBasePath(): Promise { - const modelConfigPath = getModelConfigPath(); - return readBasePathFromConfig(modelConfigPath); + const modelConfigPath = ComfyServerConfig.configPath; + return ComfyServerConfig.readBasePathFromConfig(modelConfigPath); } export async function getPythonInstallPath(): Promise { diff --git a/src/main.ts b/src/main.ts index bf96e2d5..f5caf629 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import log from 'electron-log/main'; import * as Sentry from '@sentry/electron/main'; import * as net from 'net'; import { graphics } from 'systeminformation'; -import { createModelConfigFiles, getModelConfigPath } from './config/extra_model_config'; +import { ComfyServerConfig } from './config/comfyServerConfig'; import todesktop from '@todesktop/runtime'; import { DownloadManager } from './models/DownloadManager'; import { getModelsDirectory } from './utils'; @@ -167,8 +167,7 @@ if (!gotTheLock) { }); ipcMain.on(IPC_CHANNELS.INSTALL_COMFYUI, async (event, installOptions: InstallOptions) => { // Non-blocking call. The renderer will navigate to /server-start and show install progress. - handleInstall(installOptions); - serverStart(); + handleInstall(installOptions).then(serverStart); }); // Loading renderer when all handlers are registered to ensure all event listeners are set up. @@ -198,7 +197,7 @@ if (!gotTheLock) { ipcMain.handle(IPC_CHANNELS.REINSTALL, async () => { log.info('Reinstalling...'); - const modelConfigPath = getModelConfigPath(); + const modelConfigPath = ComfyServerConfig.configPath; fs.rmSync(modelConfigPath); restartApp(); }); @@ -465,15 +464,23 @@ function findAvailablePort(startPort: number, endPort: number): Promise * This means the extra_models_config.yaml file exists in the user's data directory. */ function isFirstTimeSetup(): boolean { - const extraModelsConfigPath = getModelConfigPath(); + const extraModelsConfigPath = ComfyServerConfig.configPath; log.info(`Checking if first time setup is complete. Extra models config path: ${extraModelsConfigPath}`); return !fs.existsSync(extraModelsConfigPath); } async function handleInstall(installOptions: InstallOptions) { + const migrationSource = installOptions.migrationSourcePath; + const migrationItemIds = new Set(installOptions.migrationItemIds ?? []); + const actualComfyDirectory = ComfyConfigManager.setUpComfyUI(installOptions.installPath); - const modelConfigPath = getModelConfigPath(); - await createModelConfigFiles(modelConfigPath, actualComfyDirectory); + + const { comfyui: comfyuiConfig, ...extraConfigs } = await ComfyServerConfig.getMigrationConfig( + migrationSource, + migrationItemIds + ); + comfyuiConfig['base_path'] = actualComfyDirectory; + await ComfyServerConfig.createConfigFile(ComfyServerConfig.configPath, comfyuiConfig, extraConfigs); } async function serverStart() { @@ -510,9 +517,8 @@ async function serverStart() { appWindow.send(IPC_CHANNELS.LOG_MESSAGE, data); }, }); - const modelConfigPath = getModelConfigPath(); sendProgressUpdate(ProgressStatus.STARTING_SERVER); - await launchPythonServer(virtualEnvironment, appResourcesPath, modelConfigPath, basePath); + await launchPythonServer(virtualEnvironment, appResourcesPath, ComfyServerConfig.configPath, basePath); } else { sendProgressUpdate(ProgressStatus.READY); loadComfyIntoMainWindow(); diff --git a/tests/unit/comfyServerConfig.test.ts b/tests/unit/comfyServerConfig.test.ts new file mode 100644 index 00000000..db5ad023 --- /dev/null +++ b/tests/unit/comfyServerConfig.test.ts @@ -0,0 +1,173 @@ +// Mock electron +jest.mock('electron', () => ({ + app: { + getPath: jest.fn().mockReturnValue('/fake/user/data'), + }, +})); + +import { app } from 'electron'; +import path from 'path'; +import { ComfyServerConfig } from '../../src/config/comfyServerConfig'; +import * as fsPromises from 'node:fs/promises'; + +describe('ComfyServerConfig', () => { + describe('configPath', () => { + it('should return the correct path', () => { + // Mock the userData path + const mockUserDataPath = '/fake/user/data'; + (app.getPath as jest.Mock).mockImplementation((key: string) => { + if (key === 'userData') { + return mockUserDataPath; + } + throw new Error(`Unexpected getPath key: ${key}`); + }); + + // Access the static property + const result = ComfyServerConfig.configPath; + + // Verify the path is correctly joined + expect(result).toBe(path.join(mockUserDataPath, 'extra_models_config.yaml')); + + // Verify app.getPath was called with correct argument + expect(app.getPath).toHaveBeenCalledWith('userData'); + }); + }); + + describe('readBasePathFromConfig', () => { + const testConfigPath = path.join(__dirname, 'test_config.yaml'); + + beforeAll(async () => { + // Create a test YAML file + const testConfig = `# Test ComfyUI config +comfyui: + base_path: ~/test/comfyui + is_default: true + checkpoints: models/checkpoints/ + loras: models/loras/`; + + await fsPromises.writeFile(testConfigPath, testConfig, 'utf8'); + }); + + afterAll(async () => { + await fsPromises.rm(testConfigPath); + }); + + it('should read base_path from valid config file', async () => { + const result = await ComfyServerConfig.readBasePathFromConfig(testConfigPath); + expect(result).toBe('~/test/comfyui'); + }); + + it('should return null for non-existent file', async () => { + const result = await ComfyServerConfig.readBasePathFromConfig('non_existent_file.yaml'); + expect(result).toBeNull(); + }); + + it('should return null for invalid config file', async () => { + const invalidConfigPath = path.join(__dirname, 'invalid_config.yaml'); + await fsPromises.writeFile(invalidConfigPath, 'invalid: yaml: content:', 'utf8'); + + const result = await ComfyServerConfig.readBasePathFromConfig(invalidConfigPath); + expect(result).toBeNull(); + + await fsPromises.rm(invalidConfigPath); + }); + }); + + describe('mergeConfig', () => { + it('should merge configs with overlapping keys by concatenating paths', () => { + const baseConfig = { + checkpoints: '/base/path/checkpoints/', + loras: '/base/path/loras/', + }; + + const customConfig = { + checkpoints: '/custom/path/checkpoints/', + loras: '/custom/path/loras/', + }; + + const result = ComfyServerConfig.mergeConfig(baseConfig, customConfig); + + expect(result).toEqual({ + checkpoints: '/base/path/checkpoints/\n/custom/path/checkpoints/', + loras: '/base/path/loras/\n/custom/path/loras/', + }); + }); + + it('should preserve unique keys from custom config', () => { + const baseConfig = { + checkpoints: '/base/path/checkpoints/', + }; + + const customConfig = { + checkpoints: '/custom/path/checkpoints/', + newKey: '/custom/path/newKey/', + }; + + const result = ComfyServerConfig.mergeConfig(baseConfig, customConfig); + + expect(result).toEqual({ + checkpoints: '/base/path/checkpoints/\n/custom/path/checkpoints/', + newKey: '/custom/path/newKey/', + }); + }); + + it('should handle empty custom config', () => { + const baseConfig = { + checkpoints: '/base/path/checkpoints/', + loras: '/base/path/loras/', + }; + + const customConfig = {}; + + const result = ComfyServerConfig.mergeConfig(baseConfig, customConfig); + + expect(result).toEqual(baseConfig); + }); + + it('should handle empty base config', () => { + const baseConfig = {}; + + const customConfig = { + checkpoints: '/custom/path/checkpoints/', + loras: '/custom/path/loras/', + }; + + const result = ComfyServerConfig.mergeConfig(baseConfig, customConfig); + + expect(result).toEqual(customConfig); + }); + }); + + describe('getMigrationConfig', () => { + it('should return empty object when no migration source is provided', async () => { + const result = await ComfyServerConfig.getMigrationConfig(undefined); + expect(result).toEqual({}); + }); + + it('should merge configs and remove custom_nodes when migration source is provided', async () => { + // Mock the getConfigFromRepoPath and getBaseModelPathsFromRepoPath methods + const mockServerConfig = { + comfyui: { + checkpoints: '/server/path/checkpoints/', + custom_nodes: '/server/path/custom_nodes/', + }, + }; + + jest.spyOn(ComfyServerConfig, 'getConfigFromRepoPath').mockResolvedValue(mockServerConfig); + jest.spyOn(ComfyServerConfig, 'getBaseModelPathsFromRepoPath').mockReturnValue({ + checkpoints: '/base/path/checkpoints/', + custom_nodes: '/base/path/custom_nodes/', + }); + + const result = await ComfyServerConfig.getMigrationConfig('/fake/path', new Set(['models'])); + + expect(result.comfyui).toBeDefined(); + expect(result.comfyui.checkpoints).toBe('/server/path/checkpoints/\n/base/path/checkpoints/'); + expect(result.comfyui.custom_nodes).toBeUndefined(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); +}); diff --git a/tests/unit/extra_model_config.test.ts b/tests/unit/extra_model_config.test.ts deleted file mode 100644 index 4e12932f..00000000 --- a/tests/unit/extra_model_config.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { app } from 'electron'; -import path from 'path'; -import { getModelConfigPath, readBasePathFromConfig } from '../../src/config/extra_model_config'; -import * as fsPromises from 'node:fs/promises'; -// Mock electron -jest.mock('electron', () => ({ - app: { - getPath: jest.fn(), - }, -})); - -describe('getModelConfigPath', () => { - it('should return the correct path', async () => { - // Mock the userData path - const mockUserDataPath = '/fake/user/data'; - (app.getPath as jest.Mock).mockImplementation((key: string) => { - if (key === 'userData') { - return mockUserDataPath; - } - throw new Error(`Unexpected getPath key: ${key}`); - }); - - const result = getModelConfigPath(); - - // Verify the path is correctly joined - expect(result).toBe(path.join(mockUserDataPath, 'extra_models_config.yaml')); - - // Verify app.getPath was called with correct argument - expect(app.getPath).toHaveBeenCalledWith('userData'); - }); -}); - -describe('readBasePathFromConfig', () => { - const testConfigPath = path.join(__dirname, 'test_config.yaml'); - - beforeAll(async () => { - // Create a test YAML file - const testConfig = `# Test ComfyUI config -comfyui: - base_path: ~/test/comfyui - is_default: true - checkpoints: models/checkpoints/ - loras: models/loras/`; - - await fsPromises.writeFile(testConfigPath, testConfig, 'utf8'); - }); - - afterAll(async () => { - await fsPromises.rm(testConfigPath); - }); - - it('should read base_path from valid config file', async () => { - const result = await readBasePathFromConfig(testConfigPath); - expect(result).toBe('~/test/comfyui'); - }); - - it('should return null for non-existent file', async () => { - const result = await readBasePathFromConfig('non_existent_file.yaml'); - expect(result).toBeNull(); - }); - - it('should return null for invalid config file', async () => { - const invalidConfigPath = path.join(__dirname, 'invalid_config.yaml'); - await fsPromises.writeFile(invalidConfigPath, 'invalid: yaml: content:', 'utf8'); - - const result = await readBasePathFromConfig(invalidConfigPath); - expect(result).toBeNull(); - - await fsPromises.rm(invalidConfigPath); - }); -}); diff --git a/tests/unit/install/resourcePath.test.ts b/tests/unit/install/resourcePath.test.ts deleted file mode 100644 index 50be268b..00000000 --- a/tests/unit/install/resourcePath.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { app } from 'electron'; -import path from 'path'; -import { getBasePath, getPythonInstallPath, getAppResourcesPath } from '../../../src/install/resourcePaths'; -import { getModelConfigPath, readBasePathFromConfig } from '../../../src/config/extra_model_config'; - -// Mock the external modules -jest.mock('electron', () => ({ - app: { - isPackaged: false, - getAppPath: jest.fn(), - }, -})); - -jest.mock('../../../src/config/extra_model_config', () => ({ - getModelConfigPath: jest.fn(), - readBasePathFromConfig: jest.fn(), -})); - -describe('resourcePaths', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getBasePath', () => { - it('should return the base path from config', async () => { - const mockConfigPath = '/mock/config/path'; - const mockBasePath = '/mock/base/path'; - - (getModelConfigPath as jest.Mock).mockReturnValue(mockConfigPath); - (readBasePathFromConfig as jest.Mock).mockResolvedValue(mockBasePath); - - const result = await getBasePath(); - - expect(getModelConfigPath).toHaveBeenCalled(); - expect(readBasePathFromConfig).toHaveBeenCalledWith(mockConfigPath); - expect(result).toBe(mockBasePath); - }); - }); - - describe('getPythonInstallPath', () => { - it('should return assets path when app is not packaged', async () => { - const mockAppPath = '/mock/app/path'; - (app.getAppPath as jest.Mock).mockReturnValue(mockAppPath); - (app.isPackaged as boolean) = false; - - const result = await getPythonInstallPath(); - - expect(result).toBe(path.join(mockAppPath, 'assets')); - expect(app.getAppPath).toHaveBeenCalled(); - }); - - it('should return base path when app is packaged', async () => { - const mockBasePath = '/mock/base/path'; - (app.isPackaged as boolean) = true; - (getModelConfigPath as jest.Mock).mockReturnValue('/mock/config'); - (readBasePathFromConfig as jest.Mock).mockResolvedValue(mockBasePath); - - const result = await getPythonInstallPath(); - - expect(result).toBe(mockBasePath); - }); - }); - - describe('getAppResourcesPath', () => { - it('should return assets path when app is not packaged', async () => { - const mockAppPath = '/mock/app/path'; - (app.getAppPath as jest.Mock).mockReturnValue(mockAppPath); - (app.isPackaged as boolean) = false; - - const result = await getAppResourcesPath(); - - expect(result).toBe(path.join(mockAppPath, 'assets')); - expect(app.getAppPath).toHaveBeenCalled(); - }); - - it('should return resources path when app is packaged', async () => { - (app.isPackaged as boolean) = true; - const mockResourcesPath = '/mock/resources/path'; - (process as any).resourcesPath = mockResourcesPath; - - const result = await getAppResourcesPath(); - - expect(result).toBe(mockResourcesPath); - }); - }); -});