import { beforeEach, describe, expect, it, vi } from 'vitest';
function memoryStorage(): Storage {
const values = new Map<string, string>();
return {
get length() {
return values.size;
},
clear: () => values.clear(),
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => void values.delete(key),
setItem: (key, value) => void values.set(key, String(value)),
} as Storage;
}
describe('persistence client bootstrap', () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it('configures the runtime, document and asset HTTP stores together, in every browser', async () => {
vi.stubGlobal('window', {});
vi.stubGlobal('localStorage', memoryStorage());
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
Response.json({ learnerKey: 'anon:server-derived' }, { status: 200 }),
);
vi.stubGlobal('fetch', fetchMock);
const { HttpAssetStore, HttpDocumentStore } = await import('@openmaic/storage');
const { HttpRuntimeStore } = await import('@openmaic/storage/runtime/http');
const runtime = await import('@/lib/runtime/store');
const documents = await import('@/lib/document-store');
const assets = await import('@/lib/media/asset-pool-config');
expect(runtime.isRuntimeStorageConfigured()).toBe(true);
expect(documents.isDocumentStorageConfigured()).toBe(true);
expect(assets.isAssetPoolStorageConfigured()).toBe(true);
const runtimeStore = runtime.getRuntimeStore();
const documentStore = documents.getDocumentStore();
expect(runtimeStore).toBeInstanceOf(HttpRuntimeStore);
expect(documentStore).toBeInstanceOf(HttpDocumentStore);
const documentInternals = documentStore as unknown as {
validateSceneFn: unknown;
validateStageFn: unknown;
};
expect(documentInternals.validateSceneFn).toBe(documents.validateAppScene);
expect(documentInternals.validateStageFn).toBe(documents.validateAppStage);
for (const store of [runtimeStore, documentStore]) {
expect((store as unknown as { headersHook?: unknown }).headersHook).toBeUndefined();
}
const { getLearnerKey } = await import('@/lib/runtime/learner-key');
await expect(getLearnerKey()).resolves.toBe('anon:server-derived');
await expect(getLearnerKey()).resolves.toBe('anon:server-derived');
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/persistence/learner-key');
expect(localStorage.length).toBe(0);
const assetStore = assets.resolveConfiguredAssetPoolStore();
expect(assetStore).toBeInstanceOf(HttpAssetStore);
expect((assetStore as unknown as { baseUrl: string }).baseUrl).toBe('/api/persistence');
expect((assetStore as unknown as { headersHook?: unknown }).headersHook).toBeUndefined();
runtime.resetRuntimeStorageForTests();
documents.resetDocumentStorageForTests();
assets.resetAssetPoolStorageForTests();
expect(runtime.isRuntimeStorageConfigured()).toBe(false);
expect(documents.isDocumentStorageConfigured()).toBe(false);
expect(assets.isAssetPoolStorageConfigured()).toBe(false);
});
it('does not cache a failed learner-key request', async () => {
vi.stubGlobal('window', {});
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response('unavailable', { status: 503 }))
.mockResolvedValueOnce(Response.json({ learnerKey: 'anon:retried' }));
vi.stubGlobal('fetch', fetchMock);
const { getPersistenceLearnerKey } = await import('@/lib/persistence/bootstrap');
await expect(getPersistenceLearnerKey()).rejects.toThrow('503');
await expect(getPersistenceLearnerKey()).resolves.toBe('anon:retried');
});
it('refuses a malformed learner-key answer', async () => {
vi.stubGlobal('window', {});
vi.stubGlobal(
'fetch',
vi.fn(async () => Response.json({ learnerKey: '' })),
);
const { getPersistenceLearnerKey } = await import('@/lib/persistence/bootstrap');
await expect(getPersistenceLearnerKey()).rejects.toThrow('malformed');
});
it('does not run client configuration during server module evaluation', async () => {
const runtime = await import('@/lib/runtime/store');
const documents = await import('@/lib/document-store');
const assets = await import('@/lib/media/asset-pool-config');
expect(runtime.isRuntimeStorageConfigured()).toBe(false);
expect(documents.isDocumentStorageConfigured()).toBe(false);
expect(assets.isAssetPoolStorageConfigured()).toBe(false);
});
it('preflights both configured seams so a failure cannot partially configure bootstrap', async () => {
vi.stubGlobal('window', {});
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const documents = await import('@/lib/document-store/config');
documents.configureDocumentStorage({});
const runtime = await import('@/lib/runtime/store');
const assets = await import('@/lib/media/asset-pool-config');
expect(runtime.isRuntimeStorageConfigured()).toBe(false);
expect(documents.isDocumentStorageConfigured()).toBe(true);
expect(assets.isAssetPoolStorageConfigured()).toBe(false);
expect(errorSpy).toHaveBeenCalledOnce();
expect(errorSpy.mock.calls[0]?.[0]).toContain('FATAL');
});
it('preflights the asset seam too, so a sealed pool cannot half-configure bootstrap', async () => {
vi.stubGlobal('window', {});
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const assets = await import('@/lib/media/asset-pool-config');
assets.configureAssetPoolStorage({});
const runtime = await import('@/lib/runtime/store');
const documents = await import('@/lib/document-store/config');
expect(runtime.isRuntimeStorageConfigured()).toBe(false);
expect(documents.isDocumentStorageConfigured()).toBe(false);
expect(errorSpy).toHaveBeenCalled();
for (const call of errorSpy.mock.calls) expect(String(call[0])).toContain('FATAL');
});
});