Skip to content

Commit

Permalink
feat: create notification on user catalog issue (#1413)
Browse files Browse the repository at this point in the history
* feat: create notification on user catalog issue

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* Apply suggestions from @lstocchi

Co-authored-by: Luca Stocchi <49404737+lstocchi@users.noreply.github.com>
Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* Apply suggestions from @jeffmaury

Co-authored-by: Jeff MAURY <jmaury@redhat.com>
Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

---------

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>
Co-authored-by: Luca Stocchi <49404737+lstocchi@users.noreply.github.com>
Co-authored-by: Jeff MAURY <jmaury@redhat.com>
  • Loading branch information
3 people committed Jul 23, 2024
1 parent e886640 commit 0a9a4ca
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 25 deletions.
25 changes: 25 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Migration guide

## ApplicationCatalog

Before **Podman AI Lab** `v1.2.0` the [user-catalog](./PACKAGING-GUIDE.md#applicationcatalog) was not versioned.
Starting from `v1.2.0` the user-catalog require to have a `version` property.

The list of catalog versions can be found in [packages/backend/src/utils/catalogUtils.ts](https://github.com/containers/podman-desktop-extension-ai-lab/blob/main/packages/backend/src/utils/catalogUtils.ts)

The catalog has its own version number, as we may not require to update it with every update. It will follow semantic versioning convention.

## `None` to Catalog `1.0`

`None` represents any catalog version prior to the first versioning.

Version `1.0` of the catalog adds an important property to models `backend`, defining the type of framework required by the model to run (E.g. LLamaCPP, WhisperCPP).

### Changelog

- property `backend` on models and recipes. Ref https://github.com/containers/podman-desktop-extension-ai-lab/pull/1186
- property `models` is now **deprecated** and useless. Ref https://github.com/containers/podman-desktop-extension-ai-lab/pull/1210
- property `recommended` has been added. Ref https://github.com/containers/podman-desktop-extension-ai-lab/pull/1210
- optional `chatFormat` added. Ref: https://github.com/containers/podman-desktop-extension-ai-lab/pull/868
- optional `sha256` property on models. Ref https://github.com/containers/podman-desktop-extension-ai-lab/pull/1078

65 changes: 51 additions & 14 deletions packages/backend/src/managers/catalogManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import content from '../tests/ai-test.json';
import userContent from '../tests/ai-user-test.json';
import { type Webview, EventEmitter } from '@podman-desktop/api';
import { type Webview, EventEmitter, window } from '@podman-desktop/api';
import { CatalogManager } from './catalogManager';

import type { Stats } from 'node:fs';
Expand Down Expand Up @@ -62,6 +62,7 @@ vi.mock('@podman-desktop/api', async () => {
EventEmitter: vi.fn(),
window: {
withProgress: mocks.withProgressMock,
showNotification: vi.fn(),
},
ProgressLocation: {
TASK_WIDGET: 'TASK_WIDGET',
Expand All @@ -82,26 +83,29 @@ beforeEach(async () => {
vi.resetAllMocks();

const appUserDirectory = '.';

vi.mock('node:fs');

// mock EventEmitter logic
vi.mocked(EventEmitter).mockImplementation(() => {
const listeners: ((value: unknown) => void)[] = [];
return {
event: vi.fn().mockImplementation(callback => {
listeners.push(callback);
}),
fire: vi.fn().mockImplementation((content: unknown) => {
listeners.forEach(listener => listener(content));
}),
} as unknown as EventEmitter<unknown>;
});

// Creating CatalogManager
catalogManager = new CatalogManager(
{
postMessage: vi.fn().mockResolvedValue(undefined),
} as unknown as Webview,
appUserDirectory,
);

vi.mock('node:fs');

const listeners: ((value: unknown) => void)[] = [];

vi.mocked(EventEmitter).mockReturnValue({
event: vi.fn().mockImplementation(callback => {
listeners.push(callback);
}),
fire: vi.fn().mockImplementation((content: unknown) => {
listeners.forEach(listener => listener(content));
}),
} as unknown as EventEmitter<unknown>);
});

describe('invalid user catalog', () => {
Expand Down Expand Up @@ -263,3 +267,36 @@ test('catalog should use user items in favour of default', async () => {
test('default catalog should have latest version', () => {
expect(version).toBe(CatalogFormat.CURRENT);
});

test('wrong catalog version should create a notification', () => {
catalogManager['onUserCatalogUpdate']({ version: CatalogFormat.UNKNOWN });

expect(window.showNotification).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Incompatible user-catalog',
}),
);
});

test('malformed catalog should create a notification', async () => {
vi.mocked(existsSync).mockReturnValue(false);
vi.spyOn(path, 'resolve').mockReturnValue('path');

catalogManager['onUserCatalogUpdate']({
version: CatalogFormat.CURRENT,
models: [
{
fakeProperty: 'hello',
},
],
recipes: [],
categories: [],
});

expect(window.showNotification).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Error loading the user catalog',
body: 'Something went wrong while trying to load the user catalog: Error: invalid model format',
}),
);
});
40 changes: 29 additions & 11 deletions packages/backend/src/managers/catalogManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import defaultCatalog from '../assets/ai.json';
import type { Recipe } from '@shared/src/models/IRecipe';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import { Messages } from '@shared/Messages';
import { type Disposable, type Event, EventEmitter, type Webview } from '@podman-desktop/api';
import { type Disposable, type Event, EventEmitter, type Webview, window } from '@podman-desktop/api';
import { JsonWatcher } from '../utils/JsonWatcher';
import { Publisher } from '../utils/Publisher';
import type { LocalModelImportInfo } from '@shared/src/models/ILocalModelInfo';
Expand All @@ -37,7 +37,8 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
readonly onUpdate: Event<ApplicationCatalog> = this._onUpdate.event;

private catalog: ApplicationCatalog;
#disposables: Disposable[];
#jsonWatcher: JsonWatcher<ApplicationCatalog> | undefined;
#notification: Disposable | undefined;

constructor(
webview: Webview,
Expand All @@ -51,25 +52,21 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
models: [],
recipes: [],
};

this.#disposables = [];
}

/**
* The init method will start a watcher on the user catalog.json
*/
init(): void {
// Creating a json watcher
const jsonWatcher: JsonWatcher<unknown> = new JsonWatcher(this.getUserCatalogPath(), {
this.#jsonWatcher = new JsonWatcher(this.getUserCatalogPath(), {
version: CatalogFormat.CURRENT,
recipes: [],
models: [],
categories: [],
});
jsonWatcher.onContentUpdated(content => this.onUserCatalogUpdate(content));
jsonWatcher.init();

this.#disposables.push(jsonWatcher);
this.#jsonWatcher.onContentUpdated(content => this.onUserCatalogUpdate(content));
this.#jsonWatcher.init();
}

private loadDefaultCatalog(): void {
Expand All @@ -91,6 +88,15 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis

if (userCatalogFormat !== CatalogFormat.CURRENT) {
this.loadDefaultCatalog();
if (!this.#notification) {
this.#notification = window.showNotification({
type: 'error',
title: 'Incompatible user-catalog',
body: `The catalog is using an older version of the catalog incompatible with current version ${CatalogFormat.CURRENT}.`,
markdownActions:
':button[See migration guide]{href=https://github.com/containers/podman-desktop-extension-ai-lab/blob/main/MIGRATION.md title="Migration guide"}',
});
}
console.error(
`the user-catalog provided is using version ${userCatalogFormat} expected ${CatalogFormat.CURRENT}. You can follow the migration guide.`,
);
Expand All @@ -100,8 +106,19 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
// merging default catalog with user catalog
try {
this.catalog = merge(sanitize(defaultCatalog), sanitize({ ...content, version: userCatalogFormat }));

// reset notification if everything went smoothly
this.#notification?.dispose();
this.#notification = undefined;
} catch (err: unknown) {
console.warn(err);
if (!this.#notification) {
this.#notification = window.showNotification({
type: 'error',
title: 'Error loading the user catalog',
body: `Something went wrong while trying to load the user catalog: ${String(err)}`,
});
}
console.error(err);
this.loadDefaultCatalog();
}

Expand All @@ -114,7 +131,8 @@ export class CatalogManager extends Publisher<ApplicationCatalog> implements Dis
}

dispose(): void {
this.#disposables.forEach(watcher => watcher.dispose());
this.#jsonWatcher?.dispose();
this.#notification?.dispose();
}

public getCatalog(): ApplicationCatalog {
Expand Down

0 comments on commit 0a9a4ca

Please sign in to comment.