/** * @module tests/unit/storage/storage-path-resolution * @description Acceptance suite for the consolidated filesystem storage-path * surface (8.0). Brainy resolves the on-disk root through ONE function, * {@link resolveFilesystemRoot}. 8.0 is a clean break: `path` is the ONE * supported key (the rest of the API already speaks it: `persist(path)`, * `Brainy.load(path)`, `asOf(path)`, `restore(path)`). The pre-8.0 aliases * (`rootDirectory`, `options.*`, `fileSystemStorage.*`) were REMOVED and now * THROW with the rename — never a silent default that would misplace data. * Three things must hold and are pinned here: * 1. CLEAN BREAK — `path` resolves; any removed alias throws. * 2. TYPE INFERENCE — a bare `path` (no `type`) implies filesystem. * 3. COR-SAFETY — a plugin storage factory receives the RESOLVED `path`, so a * native side (mmap / getBinaryBlobPath) can never split-brain onto a * different directory than the one brainy itself uses. */ import { describe, it, expect, afterEach } from 'vitest' import { resolveFilesystemRoot, createStorage, DEFAULT_FILESYSTEM_ROOT } from '../../../src/storage/storageFactory.js' import { Brainy } from '../../../src/brainy.js' import type { BrainyPlugin, BrainyPluginContext, StorageAdapterFactory } from '../../../src/plugin.js' import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js' import type { StorageAdapter } from '../../../src/coreTypes.js' /** Read the resolved root directory off a concrete FileSystemStorage. */ function rootDirOf(storage: unknown): string { return (storage as { rootDir: string }).rootDir } const TARGET = '/tmp/brainy-resolve-target' describe('resolveFilesystemRoot — clean break: path resolves, removed aliases throw', () => { it('resolves the canonical top-level path', () => { expect(resolveFilesystemRoot({ path: TARGET })).toBe(TARGET) }) it.each([ ['rootDirectory', { rootDirectory: TARGET }], ['options.path', { options: { path: TARGET } }], ['options.rootDirectory', { options: { rootDirectory: TARGET } }], ['fileSystemStorage.path', { fileSystemStorage: { path: TARGET } }], ['fileSystemStorage.rootDirectory', { fileSystemStorage: { rootDirectory: TARGET } }] ])('throws (naming `path`) for the removed alias %s', (_label, config) => { expect(() => resolveFilesystemRoot(config as any)).toThrow(/removed in 8\.0|'path'/) }) it('a removed alias throws rather than silently falling through to the default', () => { // The footgun this guards: a 7.x `{ rootDirectory }` config must NOT land // on ./brainy-data on upgrade — it must fail loudly with the rename. expect(() => resolveFilesystemRoot({ rootDirectory: TARGET } as any)).toThrow(/'path'/) }) it('the canonical path wins and does NOT throw even if a stale alias is also present', () => { expect(resolveFilesystemRoot({ path: '/win', rootDirectory: '/lose' } as any)).toBe('/win') }) }) describe('resolveFilesystemRoot — zero-config default', () => { it('returns ./brainy-data when no directory is supplied', () => { expect(resolveFilesystemRoot({})).toBe(DEFAULT_FILESYSTEM_ROOT) expect(resolveFilesystemRoot({})).toBe('./brainy-data') }) it('returns ./brainy-data for type:filesystem with no path ("persist, default location")', () => { expect(resolveFilesystemRoot({ type: 'filesystem' })).toBe('./brainy-data') }) it('ignores empty-string paths and falls through to the default', () => { expect(resolveFilesystemRoot({ path: '' } as any)).toBe('./brainy-data') }) }) describe('createStorage — type inference (path implies filesystem)', () => { it('a bare { path } (no type) produces a FileSystemStorage at that path', async () => { const storage = await createStorage({ path: TARGET }) expect(storage.constructor.name).toBe('FileSystemStorage') expect(rootDirOf(storage)).toBe(TARGET) }) it('a bare { rootDirectory } (removed alias) throws via createStorage', async () => { await expect(createStorage({ rootDirectory: TARGET } as any)).rejects.toThrow( /removed in 8\.0|'path'/ ) }) it('an explicit type:filesystem with no path lands on ./brainy-data', async () => { const storage = await createStorage({ type: 'filesystem' }) expect(storage.constructor.name).toBe('FileSystemStorage') expect(rootDirOf(storage)).toBe('./brainy-data') }) it('type:memory always produces MemoryStorage regardless of path', async () => { const storage = await createStorage({ type: 'memory', path: TARGET } as any) expect(storage.constructor.name).toBe('MemoryStorage') }) }) /** * A boundary-safe fake plugin storage factory. It is NOT @soulcraft/cor — it * stands in for any native/plugin storage backend that re-resolves the on-disk * directory itself. It records the EXACT config object handed to `create()` so * the test can assert brainy normalized the canonical `path` before the handoff. */ class RecordingStorageFactory implements StorageAdapterFactory { name = 'recording-filesystem' received: Record | null = null create(config: Record): StorageAdapter { this.received = config return new MemoryStorage() as unknown as StorageAdapter } } /** A minimal plugin that registers the recording factory under `storage:filesystem`. */ function makeRecordingPlugin(factory: RecordingStorageFactory): BrainyPlugin { return { name: 'test-recording-storage', async activate(ctx: BrainyPluginContext): Promise { ctx.registerProvider('storage:filesystem', factory) return true } } } describe('cor-safety — plugin storage factory receives the RESOLVED path', () => { const brains: Brainy[] = [] afterEach(async () => { for (const b of brains.splice(0)) { try { await b.close() } catch { /* best-effort */ } } }) it('normalizes a canonical top-level { path } before handing it to the factory', async () => { const factory = new RecordingStorageFactory() const brain = new Brainy({ requireSubtype: false, silent: true, storage: { type: 'filesystem', path: TARGET } }) brain.use(makeRecordingPlugin(factory)) brains.push(brain) await brain.init() expect(factory.received).not.toBeNull() expect(factory.received!.path).toBe(TARGET) }) it('a removed { rootDirectory } alias throws at init and never reaches the factory', async () => { // Clean break: the normalize step (resolveFilesystemRoot) throws on the // removed alias BEFORE the plugin factory is ever called — no split-brain, // no silent ./brainy-data. const factory = new RecordingStorageFactory() const brain = new Brainy({ requireSubtype: false, silent: true, storage: { type: 'filesystem', rootDirectory: TARGET } }) brain.use(makeRecordingPlugin(factory)) brains.push(brain) await expect(brain.init()).rejects.toThrow(/removed in 8\.0|'path'/) expect(factory.received).toBeNull() }) })