fix: refresh skills catalog after settings updates (#1681)
This commit is contained in:
@@ -1,10 +1,41 @@
|
||||
import { afterAll, beforeEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { applyPersistedHomeDirectoryToWindow } from './persistence';
|
||||
import type { RuntimeAPIs, SettingsPayload } from '@/lib/api/types';
|
||||
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { applyPersistedHomeDirectoryToWindow, updateDesktopSettings } from './persistence';
|
||||
|
||||
type TestWindow = { __OPENCHAMBER_HOME__?: string };
|
||||
type TestWindow = {
|
||||
__OPENCHAMBER_HOME__?: string;
|
||||
dispatchEvent: (event: Event) => boolean;
|
||||
};
|
||||
|
||||
let createdWindow = false;
|
||||
let createdLocalStorage = false;
|
||||
|
||||
const ensureLocalStorage = (): void => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = new Map<string, string>();
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
values.set(key, value);
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
values.delete(key);
|
||||
},
|
||||
clear: () => {
|
||||
values.clear();
|
||||
},
|
||||
},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
createdLocalStorage = true;
|
||||
};
|
||||
|
||||
const getWindow = (): TestWindow => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -15,22 +46,41 @@ const getWindow = (): TestWindow => {
|
||||
});
|
||||
createdWindow = true;
|
||||
}
|
||||
return window as unknown as TestWindow;
|
||||
const testWindow = window as unknown as Partial<TestWindow>;
|
||||
testWindow.dispatchEvent ??= () => true;
|
||||
ensureLocalStorage();
|
||||
return testWindow as TestWindow;
|
||||
};
|
||||
|
||||
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const registerSettingsSave = (save: (changes: Partial<SettingsPayload>) => Promise<SettingsPayload>): void => {
|
||||
registerRuntimeAPIs({
|
||||
runtime: { platform: 'web', isDesktop: false, isVSCode: false },
|
||||
settings: {
|
||||
load: async () => ({ settings: {}, source: 'web' }),
|
||||
save,
|
||||
},
|
||||
} as unknown as RuntimeAPIs);
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
registerRuntimeAPIs(null);
|
||||
if (createdWindow) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else if (typeof window !== 'undefined') {
|
||||
delete getWindow().__OPENCHAMBER_HOME__;
|
||||
}
|
||||
if (createdLocalStorage) {
|
||||
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||
}
|
||||
});
|
||||
|
||||
describe('applyPersistedHomeDirectoryToWindow', () => {
|
||||
beforeEach(() => {
|
||||
delete getWindow().__OPENCHAMBER_HOME__;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (createdWindow) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else {
|
||||
delete getWindow().__OPENCHAMBER_HOME__;
|
||||
}
|
||||
});
|
||||
|
||||
test('does not overwrite an injected desktop home directory', () => {
|
||||
getWindow().__OPENCHAMBER_HOME__ = '/Users/example';
|
||||
|
||||
@@ -45,3 +95,79 @@ describe('applyPersistedHomeDirectoryToWindow', () => {
|
||||
expect(getWindow().__OPENCHAMBER_HOME__).toBe('/Users/example/projects/app');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDesktopSettings', () => {
|
||||
beforeEach(() => {
|
||||
getWindow();
|
||||
registerRuntimeAPIs(null);
|
||||
});
|
||||
|
||||
test('waits for the debounced settings save to finish before resolving', async () => {
|
||||
let saveStarted = false;
|
||||
let saveFinished = false;
|
||||
let updateResolved = false;
|
||||
|
||||
registerSettingsSave(async () => {
|
||||
saveStarted = true;
|
||||
await delay(100);
|
||||
saveFinished = true;
|
||||
return {};
|
||||
});
|
||||
|
||||
const update = updateDesktopSettings({
|
||||
skillCatalogs: [{ id: 'custom:test', label: 'Test', source: 'owner/repo' }],
|
||||
});
|
||||
update.then(() => {
|
||||
updateResolved = true;
|
||||
}).catch(() => {
|
||||
updateResolved = true;
|
||||
});
|
||||
|
||||
await delay(50);
|
||||
expect(saveStarted).toBe(false);
|
||||
expect(updateResolved).toBe(false);
|
||||
|
||||
await delay(200);
|
||||
expect(saveStarted).toBe(true);
|
||||
expect(saveFinished).toBe(false);
|
||||
expect(updateResolved).toBe(false);
|
||||
|
||||
await update;
|
||||
expect(saveFinished).toBe(true);
|
||||
expect(updateResolved).toBe(true);
|
||||
});
|
||||
|
||||
test('coalesces rapid settings updates and resolves every caller after one merged save', async () => {
|
||||
const saveCalls: Array<Partial<SettingsPayload>> = [];
|
||||
let firstResolved = false;
|
||||
let secondResolved = false;
|
||||
|
||||
registerSettingsSave(async (changes) => {
|
||||
saveCalls.push(changes);
|
||||
await delay(50);
|
||||
return {};
|
||||
});
|
||||
|
||||
const first = updateDesktopSettings({ themeVariant: 'dark' });
|
||||
first.then(() => {
|
||||
firstResolved = true;
|
||||
}).catch(() => {
|
||||
firstResolved = true;
|
||||
});
|
||||
|
||||
await delay(50);
|
||||
|
||||
const second = updateDesktopSettings({ fontSize: 14 });
|
||||
second.then(() => {
|
||||
secondResolved = true;
|
||||
}).catch(() => {
|
||||
secondResolved = true;
|
||||
});
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(saveCalls).toEqual([{ themeVariant: 'dark', fontSize: 14 }]);
|
||||
expect(firstResolved).toBe(true);
|
||||
expect(secondResolved).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user