183 lines
7.0 KiB
TypeScript
183 lines
7.0 KiB
TypeScript
import type { KVStore, RuntimeStore } from '@openmaic/storage';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
function stubStore(deleteStageRuntime = vi.fn().mockResolvedValue(undefined)): RuntimeStore {
|
|
return { deleteStageRuntime } as unknown as RuntimeStore;
|
|
}
|
|
|
|
describe('configureRuntimeStorage', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('retains the lazy BrowserRuntimeStore singleton by default', async () => {
|
|
const { BrowserRuntimeStore } = await import('@openmaic/storage');
|
|
const { getRuntimeStore } = await import('@/lib/runtime/store');
|
|
|
|
const first = getRuntimeStore();
|
|
|
|
expect(first).toBeInstanceOf(BrowserRuntimeStore);
|
|
expect(getRuntimeStore()).toBe(first);
|
|
});
|
|
|
|
it('routes an existing consumer through an injected RuntimeStore', async () => {
|
|
vi.stubGlobal('indexedDB', {
|
|
databases: vi.fn().mockResolvedValue([]),
|
|
});
|
|
const deleteStageRuntime = vi.fn().mockResolvedValue(undefined);
|
|
const injected = stubStore(deleteStageRuntime);
|
|
const { configureRuntimeStorage, deleteStageRuntimeSafely, getRuntimeStore } =
|
|
await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: injected });
|
|
|
|
await deleteStageRuntimeSafely('stage-injected');
|
|
|
|
expect(getRuntimeStore()).toBe(injected);
|
|
expect(deleteStageRuntime).toHaveBeenCalledExactlyOnceWith('stage-injected');
|
|
});
|
|
|
|
it('evaluates a store factory lazily and only once', async () => {
|
|
const injected = stubStore();
|
|
const factory = vi.fn(() => injected);
|
|
const { configureRuntimeStorage, getRuntimeStore } = await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: factory });
|
|
|
|
expect(factory).not.toHaveBeenCalled();
|
|
expect(getRuntimeStore()).toBe(injected);
|
|
expect(getRuntimeStore()).toBe(injected);
|
|
expect(factory).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('retries a store factory after it throws, then latches the first success', async () => {
|
|
const injected = stubStore();
|
|
const factory = vi
|
|
.fn<() => RuntimeStore>()
|
|
.mockImplementationOnce(() => {
|
|
throw new Error('backend is not ready');
|
|
})
|
|
.mockReturnValue(injected);
|
|
const { configureRuntimeStorage, getRuntimeStore } = await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: factory });
|
|
|
|
expect(() => getRuntimeStore()).toThrow('backend is not ready');
|
|
expect(getRuntimeStore()).toBe(injected);
|
|
expect(getRuntimeStore()).toBe(injected);
|
|
expect(factory).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('throws when configured after runtime storage has been used', async () => {
|
|
const { configureRuntimeStorage, getRuntimeStore } = await import('@/lib/runtime/store');
|
|
getRuntimeStore();
|
|
|
|
expect(() => configureRuntimeStorage({ store: stubStore() })).toThrow(
|
|
'configureRuntimeStorage must be called at module-level bootstrap, before any runtime consumer runs — a component effect is too late.',
|
|
);
|
|
});
|
|
|
|
it('explains that configuration stays sealed after a factory resolution failure', async () => {
|
|
const factory = vi.fn((): RuntimeStore => {
|
|
throw new Error('factory failed');
|
|
});
|
|
const { configureRuntimeStorage, getRuntimeStore } = await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: factory });
|
|
expect(() => getRuntimeStore()).toThrow('factory failed');
|
|
|
|
expect(() => configureRuntimeStorage({ store: stubStore() })).toThrow(
|
|
'configuration remains sealed even if resolution failed. Retry the runtime consumer to retry resolution.',
|
|
);
|
|
});
|
|
|
|
it('throws on repeated configuration before first use', async () => {
|
|
const { configureRuntimeStorage } = await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: stubStore() });
|
|
|
|
expect(() => configureRuntimeStorage({ learnerKey: () => 'account:second' })).toThrow(
|
|
'Runtime storage has already been configured',
|
|
);
|
|
});
|
|
|
|
it('gives an explicit KV store priority over the configured learnerKey provider', async () => {
|
|
const learnerKey = vi.fn(() => 'account:user-42');
|
|
const kv = {
|
|
get: vi.fn().mockResolvedValue('anon:explicit-kv'),
|
|
} as unknown as KVStore;
|
|
const { configureRuntimeStorage } = await import('@/lib/runtime/store');
|
|
const { getLearnerKey } = await import('@/lib/runtime/learner-key');
|
|
configureRuntimeStorage({ learnerKey });
|
|
|
|
await expect(getLearnerKey(kv)).resolves.toBe('anon:explicit-kv');
|
|
expect(kv.get).toHaveBeenCalledExactlyOnceWith('runtime.learnerKey', 'device');
|
|
expect(learnerKey).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('deduplicates configured learnerKey resolution and latches its first value', async () => {
|
|
let resolveProvider!: (value: string) => void;
|
|
const learnerKey = vi.fn(
|
|
() =>
|
|
new Promise<string>((resolve) => {
|
|
resolveProvider = resolve;
|
|
}),
|
|
);
|
|
const { configureRuntimeStorage } = await import('@/lib/runtime/store');
|
|
const { getLearnerKey } = await import('@/lib/runtime/learner-key');
|
|
configureRuntimeStorage({ learnerKey });
|
|
|
|
const first = getLearnerKey();
|
|
const concurrent = getLearnerKey();
|
|
await Promise.resolve();
|
|
expect(learnerKey).toHaveBeenCalledOnce();
|
|
resolveProvider('account:first');
|
|
await expect(Promise.all([first, concurrent])).resolves.toEqual([
|
|
'account:first',
|
|
'account:first',
|
|
]);
|
|
await expect(getLearnerKey()).resolves.toBe('account:first');
|
|
expect(learnerKey).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('reports and resets bootstrap configuration state for tests', async () => {
|
|
const { configureRuntimeStorage, isRuntimeStorageConfigured, resetRuntimeStorageForTests } =
|
|
await import('@/lib/runtime/store');
|
|
|
|
expect(isRuntimeStorageConfigured()).toBe(false);
|
|
configureRuntimeStorage({ learnerKey: () => 'account:test' });
|
|
expect(isRuntimeStorageConfigured()).toBe(true);
|
|
resetRuntimeStorageForTests();
|
|
expect(isRuntimeStorageConfigured()).toBe(false);
|
|
expect(() => configureRuntimeStorage({ store: stubStore() })).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('configuration snapshot and full reset', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('mutating the options object after configure has no effect', async () => {
|
|
const first = stubStore();
|
|
const second = stubStore();
|
|
const { configureRuntimeStorage, getRuntimeStore } = await import('@/lib/runtime/store');
|
|
const options = { store: first };
|
|
configureRuntimeStorage(options);
|
|
options.store = second;
|
|
|
|
expect(getRuntimeStore()).toBe(first);
|
|
});
|
|
|
|
it('resetRuntimeStorageForTests clears the latched store singleton', async () => {
|
|
const first = stubStore();
|
|
const second = stubStore();
|
|
const { configureRuntimeStorage, getRuntimeStore, resetRuntimeStorageForTests } =
|
|
await import('@/lib/runtime/store');
|
|
configureRuntimeStorage({ store: first });
|
|
expect(getRuntimeStore()).toBe(first);
|
|
|
|
resetRuntimeStorageForTests();
|
|
configureRuntimeStorage({ store: second });
|
|
|
|
expect(getRuntimeStore()).toBe(second);
|
|
});
|
|
});
|