From 5096f90fbc9ba094e0af92f81a2845aa637adc9f Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 16 Jun 2026 09:46:39 -0700 Subject: [PATCH] fix(8.0): honor top-level storage.path as a rootDirectory alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `storage: { type: 'filesystem', path: '…' }` is a widely-used, doc-promoted config shape, but the 8.0 storage refactor dropped top-level `path` from the root-directory resolution chain — so it was silently ignored and every such brain wrote to the default `./brainy-data` instead. A consumer upgrading with `storage: { path: './my-data' }` would have had their data quietly relocated. Restores `path` as a first-class alias for `rootDirectory` (it already worked nested under `options.path`; now it works top-level too), adds it to the StorageOptions / BrainyConfig.storage types, and pins every accepted spelling (rootDirectory, path, options.rootDirectory, options.path, default) in a unit test of the resolution chain. --- src/storage/storageFactory.ts | 9 ++++ src/types/brainy.types.ts | 3 ++ .../unit/storage/storage-path-config.test.ts | 53 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tests/unit/storage/storage-path-config.test.ts diff --git a/src/storage/storageFactory.ts b/src/storage/storageFactory.ts index 88abf590..c001c382 100644 --- a/src/storage/storageFactory.ts +++ b/src/storage/storageFactory.ts @@ -42,6 +42,14 @@ export interface StorageOptions { /** Root directory for filesystem storage. */ rootDirectory?: string + /** + * Alias for `rootDirectory`. The `storage: { type: 'filesystem', path: '…' }` + * shape is widely used (and shown throughout the docs), so it is honored as a + * first-class top-level key — not silently ignored in favour of the default + * directory. + */ + path?: string + /** * Nested options block (backward-compat for `BrainyConfig.storage.options`). * Recognized keys: `rootDirectory`, `path`. @@ -94,6 +102,7 @@ async function pickAdapter(options: StorageOptions): Promise { async function createFilesystemStorage(options: StorageOptions): Promise { const rootDir = options.rootDirectory ?? + options.path ?? options.options?.rootDirectory ?? options.options?.path ?? './brainy-data' diff --git a/src/types/brainy.types.ts b/src/types/brainy.types.ts index 15d4072f..b0ba5978 100644 --- a/src/types/brainy.types.ts +++ b/src/types/brainy.types.ts @@ -1202,6 +1202,9 @@ export interface BrainyConfig { /** Root directory for filesystem storage. Passed through to storage factories * including plugin-provided factories (e.g. native mmap providers). */ rootDirectory?: string + /** Alias for `rootDirectory`. The `storage: { type: 'filesystem', path: '…' }` + * shape is honored as a first-class key so existing configs keep working. */ + path?: string options?: any } | StorageAdapter diff --git a/tests/unit/storage/storage-path-config.test.ts b/tests/unit/storage/storage-path-config.test.ts new file mode 100644 index 00000000..ffbfd94c --- /dev/null +++ b/tests/unit/storage/storage-path-config.test.ts @@ -0,0 +1,53 @@ +/** + * Storage root-directory resolution from `StorageOptions`. + * + * `storage: { type: 'filesystem', path: '…' }` is a widely-used, doc-promoted + * config shape. A refactor once dropped the top-level `path` key from the + * resolution chain, so it was silently ignored and every brain wrote to the + * default `./brainy-data` instead — a quiet data-misplacement footgun on + * upgrade. These tests pin every accepted spelling to the directory it must + * resolve to. + */ +import { describe, it, expect } from 'vitest' +import { createStorage } from '../../../src/storage/storageFactory.js' + +/** Read the resolved root directory off the concrete FileSystemStorage. */ +function rootDirOf(storage: unknown): string { + return (storage as { rootDir: string }).rootDir +} + +describe('createStorage — filesystem root-directory resolution', () => { + it('honors top-level rootDirectory', async () => { + const storage = await createStorage({ type: 'filesystem', rootDirectory: '/tmp/brainy-rd' }) + expect(rootDirOf(storage)).toBe('/tmp/brainy-rd') + }) + + it('honors top-level path (the documented shorthand)', async () => { + const storage = await createStorage({ type: 'filesystem', path: '/tmp/brainy-path' }) + expect(rootDirOf(storage)).toBe('/tmp/brainy-path') + }) + + it('honors nested options.rootDirectory', async () => { + const storage = await createStorage({ type: 'filesystem', options: { rootDirectory: '/tmp/brainy-ord' } }) + expect(rootDirOf(storage)).toBe('/tmp/brainy-ord') + }) + + it('honors nested options.path', async () => { + const storage = await createStorage({ type: 'filesystem', options: { path: '/tmp/brainy-opath' } }) + expect(rootDirOf(storage)).toBe('/tmp/brainy-opath') + }) + + it('prefers top-level rootDirectory over a nested options.path', async () => { + const storage = await createStorage({ + type: 'filesystem', + rootDirectory: '/tmp/brainy-win', + options: { path: '/tmp/brainy-lose' } + }) + expect(rootDirOf(storage)).toBe('/tmp/brainy-win') + }) + + it('falls back to ./brainy-data only when no directory is supplied', async () => { + const storage = await createStorage({ type: 'filesystem' }) + expect(rootDirOf(storage)).toBe('./brainy-data') + }) +})