From d73eecc69bd55c918ec1efb9e8029c83d97c56ec Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 28 May 2025 11:11:43 +0200 Subject: [PATCH 1/3] feat: read catalog synchronously Signed-off-by: Philippe Martin --- .../src/managers/catalogManager.spec.ts | 35 ++++++++++++------- .../backend/src/managers/catalogManager.ts | 17 ++++++++- packages/backend/src/studio.ts | 2 +- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/managers/catalogManager.spec.ts b/packages/backend/src/managers/catalogManager.spec.ts index bd435a5ee..fcaff021f 100644 --- a/packages/backend/src/managers/catalogManager.spec.ts +++ b/packages/backend/src/managers/catalogManager.spec.ts @@ -96,7 +96,7 @@ beforeEach(async () => { describe('invalid user catalog', () => { beforeEach(async () => { vi.mocked(promises.readFile).mockResolvedValue('invalid json'); - catalogManager.init(); + await catalogManager.init(); }); test('expect correct model is returned with valid id', () => { @@ -116,7 +116,7 @@ describe('invalid user catalog', () => { test('expect correct model is returned from default catalog with valid id when no user catalog exists', async () => { vi.mocked(existsSync).mockReturnValue(false); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getRecipes().length > 0); const model = catalogManager.getModelById('llama-2-7b-chat.Q5_K_S'); @@ -132,7 +132,7 @@ test('expect correct model is returned with valid id when the user catalog is va vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const model = catalogManager.getModelById('model1'); @@ -146,7 +146,7 @@ test('expect to call writeFile in addLocalModelsToCatalog with catalog updated', vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getRecipes().length > 0); const mtimeDate = new Date('2024-04-03T09:51:15.766Z'); @@ -174,7 +174,7 @@ test('expect to call writeFile in removeLocalModelFromCatalog with catalog updat vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); vi.mocked(path.resolve).mockReturnValue('path'); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getRecipes().length > 0); vi.mocked(promises.writeFile).mockResolvedValue(); @@ -196,7 +196,7 @@ test('catalog should be the combination of user catalog and default catalog', as vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); vi.mocked(path.resolve).mockReturnValue('path'); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().length > userContent.models.length); const mtimeDate = new Date('2024-04-03T09:51:15.766Z'); @@ -238,7 +238,7 @@ test('catalog should use user items in favour of default', async () => { vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(overwriteFullCatalog)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().length > 0); const mtimeDate = new Date('2024-04-03T09:51:15.766Z'); @@ -330,7 +330,7 @@ test('filter recipes by language', async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const result1 = catalogManager.filterRecipes({ languages: ['lang1'], @@ -375,7 +375,7 @@ test('filter recipes by tool', async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const result1 = catalogManager.filterRecipes({ @@ -445,7 +445,7 @@ test('filter recipes by framework', async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const result1 = catalogManager.filterRecipes({ @@ -519,7 +519,7 @@ test('filter recipes by language and framework', async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const result1 = catalogManager.filterRecipes({ @@ -546,7 +546,7 @@ test('filter recipes by language, tool and framework', async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); - catalogManager.init(); + await catalogManager.init(); await vi.waitUntil(() => catalogManager.getModels().some(model => model.id === 'model1')); const result1 = catalogManager.filterRecipes({ @@ -567,3 +567,14 @@ test('filter recipes by language, tool and framework', async () => { tools: [{ name: 'tool1', count: 1 }], }); }); + +test('models are loaded as soon as init is finished when no user catalog', async () => { + await catalogManager.init(); + expect(catalogManager.getModels()).toHaveLength(3); +}); + +test('models are loaded as soon as init is finished when user catalog exists', async () => { + vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); + await catalogManager.init(); + expect(catalogManager.getModels()).toHaveLength(5); +}); diff --git a/packages/backend/src/managers/catalogManager.ts b/packages/backend/src/managers/catalogManager.ts index b84f45ad5..dac98a168 100644 --- a/packages/backend/src/managers/catalogManager.ts +++ b/packages/backend/src/managers/catalogManager.ts @@ -60,7 +60,7 @@ export class CatalogManager extends Publisher implements Dis /** * The init method will start a watcher on the user catalog.json */ - init(): void { + async init(): Promise { // Creating a json watcher this.#jsonWatcher = new JsonWatcher(this.getUserCatalogPath(), { version: CatalogFormat.CURRENT, @@ -70,6 +70,21 @@ export class CatalogManager extends Publisher implements Dis }); this.#jsonWatcher.onContentUpdated(content => this.onUserCatalogUpdate(content)); this.#jsonWatcher.init(); + + await this.readCatalogs(); + } + + // read user and default catalog + private async readCatalogs(): Promise { + let content: unknown; + try { + const str = await promises.readFile(this.getUserCatalogPath(), 'utf8'); + content = JSON.parse(str); + } catch { + // Ignore all errors at this time, errors will be caught from JSON watcher event + content = {}; + } + this.onUserCatalogUpdate(content); } private loadDefaultCatalog(): void { diff --git a/packages/backend/src/studio.ts b/packages/backend/src/studio.ts index feeaa75d3..0f6f051cd 100644 --- a/packages/backend/src/studio.ts +++ b/packages/backend/src/studio.ts @@ -212,7 +212,7 @@ export class Studio { * Create catalog manager, responsible for loading the catalog files and watching for changes */ this.#catalogManager = new CatalogManager(this.#rpcExtension, appUserDirectory); - this.#catalogManager.init(); + await this.#catalogManager.init(); /** * The builder manager is handling the building tasks, create corresponding tasks From 544a64724b7524d8ef650744d3b49e4900967ce8 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 28 May 2025 14:21:20 +0200 Subject: [PATCH 2/3] feat: wrap JsonWatcher into Promise Signed-off-by: Philippe Martin --- .../src/managers/catalogManager.spec.ts | 1 + .../backend/src/managers/catalogManager.ts | 36 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/managers/catalogManager.spec.ts b/packages/backend/src/managers/catalogManager.spec.ts index fcaff021f..3fc8a20a8 100644 --- a/packages/backend/src/managers/catalogManager.spec.ts +++ b/packages/backend/src/managers/catalogManager.spec.ts @@ -575,6 +575,7 @@ test('models are loaded as soon as init is finished when no user catalog', async test('models are loaded as soon as init is finished when user catalog exists', async () => { vi.mocked(promises.readFile).mockResolvedValue(JSON.stringify(userContent)); + vi.mocked(existsSync).mockReturnValue(true); await catalogManager.init(); expect(catalogManager.getModels()).toHaveLength(5); }); diff --git a/packages/backend/src/managers/catalogManager.ts b/packages/backend/src/managers/catalogManager.ts index dac98a168..eef29c5e9 100644 --- a/packages/backend/src/managers/catalogManager.ts +++ b/packages/backend/src/managers/catalogManager.ts @@ -61,30 +61,20 @@ export class CatalogManager extends Publisher implements Dis * The init method will start a watcher on the user catalog.json */ async init(): Promise { - // Creating a json watcher - this.#jsonWatcher = new JsonWatcher(this.getUserCatalogPath(), { - version: CatalogFormat.CURRENT, - recipes: [], - models: [], - categories: [], + return new Promise(resolve => { + // Creating a json watcher + this.#jsonWatcher = new JsonWatcher(this.getUserCatalogPath(), { + version: CatalogFormat.CURRENT, + recipes: [], + models: [], + categories: [], + }); + this.#jsonWatcher.onContentUpdated(content => { + this.onUserCatalogUpdate(content); + resolve(); + }); + this.#jsonWatcher.init(); }); - this.#jsonWatcher.onContentUpdated(content => this.onUserCatalogUpdate(content)); - this.#jsonWatcher.init(); - - await this.readCatalogs(); - } - - // read user and default catalog - private async readCatalogs(): Promise { - let content: unknown; - try { - const str = await promises.readFile(this.getUserCatalogPath(), 'utf8'); - content = JSON.parse(str); - } catch { - // Ignore all errors at this time, errors will be caught from JSON watcher event - content = {}; - } - this.onUserCatalogUpdate(content); } private loadDefaultCatalog(): void { From dcbb8383406da03a439706df9a7c3f4650f4d6ed Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 28 May 2025 15:15:45 +0200 Subject: [PATCH 3/3] test: mock catalogManager Signed-off-by: Philippe Martin --- packages/backend/src/studio.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/backend/src/studio.spec.ts b/packages/backend/src/studio.spec.ts index 28cba3267..816df81b4 100644 --- a/packages/backend/src/studio.spec.ts +++ b/packages/backend/src/studio.spec.ts @@ -21,10 +21,12 @@ import { afterEach, beforeEach, expect, test, vi, describe, type MockInstance } from 'vitest'; import { Studio } from './studio'; import { type ExtensionContext, EventEmitter, version } from '@podman-desktop/api'; +import { CatalogManager } from './managers/catalogManager'; import * as fs from 'node:fs'; vi.mock('./managers/modelsManager'); +vi.mock('./managers/catalogManager'); const mockedExtensionContext = { subscriptions: [], @@ -124,6 +126,12 @@ beforeEach(() => { } as unknown as EventEmitter); mocks.postMessage.mockResolvedValue(undefined); + + vi.mocked(CatalogManager).mockReturnValue({ + onUpdate: vi.fn(), + init: vi.fn(), + getRecipes: vi.fn().mockReturnValue([]), + } as unknown as CatalogManager); }); afterEach(() => {