chore(8.0): Phase C + D + E — config simplification, TODO sweep, test race fix
Phase C: storageAutoConfig.ts rewrite (376 LOC → ~110 LOC). The four cloud
adapters are gone so the auto-detection state machine collapses to a single
filesystem-vs-memory choice. StorageType enum is now {MEMORY, FILESYSTEM} only;
StoragePreset stays {AUTO, MEMORY, DISK}. zeroConfig.ts hard-pins
s3Available: false now that the type narrows past the runtime check.
Phase D: TODO/FIXME sweep in src/. Removed seven stale markers. The CLI cow
migrate command's backup path now uses fs.cp with recursive + force:false
instead of a TODO placeholder.
Phase E: skipped-test deletion + parallel-test race fix.
- storage-batch-operations.test.ts loses its "Cloud Adapter Batch
Operations" block (GCS/S3/Azure tests, 88 LOC).
- cow-full-integration.test.ts loses its skipped "S3 adapter" test.
- create-entities-default.test.ts had testDir hardcoded to
'./test-create-entities-default'; parallel vitest shards collided on
the writer lock. testDir is now os.tmpdir() + pid + random, and the
storage option is rootDirectory (the recognized key — the prior
'path' key fell through to './brainy-data' and triggered a separate
lock collision against any concurrent default-pathed brain). Added
afterEach brain.close() so the lock releases before rmSync.
This commit is contained in:
parent
cb16a39a0c
commit
2626ab8d62
10 changed files with 94 additions and 485 deletions
|
|
@ -136,11 +136,9 @@ ${chalk.cyan('Fork Statistics:')}
|
|||
const marker = isCurrent ? chalk.green('*') : ' '
|
||||
const name = isCurrent ? chalk.green(branch) : branch
|
||||
|
||||
// Get branch info
|
||||
// TODO: Re-enable when COW is integrated into BaseStorage
|
||||
// const ref = await brain.storage.refManager.getRef(branch)
|
||||
// const age = ref ? formatAge(Date.now() - ref.updatedAt) : 'unknown'
|
||||
|
||||
// Branch-info enrichment (last-commit timestamp, etc.) requires a
|
||||
// refManager accessor that brainy hasn't surfaced on the public
|
||||
// storage adapter yet. CLI shows the bare branch name for now.
|
||||
console.log(` ${marker} ${name}`)
|
||||
}
|
||||
|
||||
|
|
@ -360,10 +358,8 @@ ${chalk.cyan('Fork Statistics:')}
|
|||
if (options.backup) {
|
||||
const backupPath = `${fromPath}.backup-${Date.now()}`
|
||||
spinner = ora(`Creating backup: ${backupPath}...`).start()
|
||||
|
||||
// TODO: Implement backup
|
||||
// await copyDirectory(fromPath, backupPath)
|
||||
|
||||
const fs = await import('node:fs/promises')
|
||||
await fs.cp(fromPath, backupPath, { recursive: true, force: false })
|
||||
spinner.succeed(`Backup created: ${chalk.green(backupPath)}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import chalk from 'chalk'
|
||||
import inquirer from 'inquirer'
|
||||
// import fuzzy from 'fuzzy' // TODO: Install fuzzy package or remove dependency
|
||||
import ora from 'ora'
|
||||
import { Brainy } from '../brainy.js'
|
||||
import { getBrainyVersion } from '../utils/version.js'
|
||||
|
|
|
|||
|
|
@ -1,377 +1,113 @@
|
|||
/**
|
||||
* Storage Configuration Auto-Detection
|
||||
* Intelligently selects storage based on environment and available services
|
||||
* @module config/storageAutoConfig
|
||||
* @description Storage configuration auto-detection for Brainy 8.0.
|
||||
*
|
||||
* Brainy 8.0 ships **two storage adapters** per `BR-BRAINY-80-STORAGE-SIMPLIFY`:
|
||||
* - `FILESYSTEM` — persistent on-disk storage (Node, Bun, Deno).
|
||||
* - `MEMORY` — in-memory, ephemeral.
|
||||
*
|
||||
* Cloud storage adapters (GCS / S3 / R2 / Azure) and the browser-only OPFS
|
||||
* adapter were removed. Cloud backup is now an operator concern handled by
|
||||
* `gsutil` / `aws s3 cp` / `rclone` / `azcopy` against the on-disk artefact.
|
||||
*
|
||||
* This module is kept thin: a two-value enum, a tiny preset enum that mirrors
|
||||
* it, and an `autoDetectStorage()` that picks filesystem when available and
|
||||
* memory otherwise.
|
||||
*/
|
||||
|
||||
import { isBrowser, isNode } from '../utils/environment.js'
|
||||
import { isNode } from '../utils/environment.js'
|
||||
|
||||
/**
|
||||
* Low-level storage implementation types
|
||||
* Storage backend types shipped in Brainy 8.0.
|
||||
*/
|
||||
export enum StorageType {
|
||||
MEMORY = 'memory',
|
||||
FILESYSTEM = 'filesystem',
|
||||
OPFS = 'opfs',
|
||||
S3 = 's3',
|
||||
GCS = 'gcs',
|
||||
GCS_NATIVE = 'gcs-native',
|
||||
R2 = 'r2'
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level storage presets (maps to StorageType)
|
||||
* High-level storage presets. `AUTO` picks filesystem when available, memory
|
||||
* otherwise; `MEMORY` and `DISK` are explicit overrides.
|
||||
*/
|
||||
export enum StoragePreset {
|
||||
AUTO = 'auto',
|
||||
MEMORY = 'memory',
|
||||
DISK = 'disk',
|
||||
CLOUD = 'cloud'
|
||||
}
|
||||
|
||||
// Backward compatibility type aliases
|
||||
export type StorageTypeString = 'memory' | 'filesystem' | 'opfs' | 's3' | 'gcs' | 'r2' | 'gcs-native'
|
||||
export type StoragePresetString = 'auto' | 'memory' | 'disk' | 'cloud'
|
||||
export type StorageTypeString = 'memory' | 'filesystem'
|
||||
export type StoragePresetString = 'auto' | 'memory' | 'disk'
|
||||
|
||||
export interface StorageConfigResult {
|
||||
type: StorageType | StorageTypeString // Support both enum and string for compatibility
|
||||
config: any
|
||||
type: StorageType | StorageTypeString
|
||||
config: { type: StorageType | StorageTypeString; rootDirectory?: string }
|
||||
reason: string
|
||||
autoSelected: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect the best storage configuration
|
||||
* @param override - Manual override: specific type or preset
|
||||
* Resolve a storage configuration. Accepts a {@link StorageType},
|
||||
* {@link StoragePreset}, plain string equivalent, or a free-form storage
|
||||
* config object. Returns a normalized result with the picked type, the
|
||||
* underlying config payload, a human-readable reason, and whether the
|
||||
* pick was auto-selected.
|
||||
*/
|
||||
export async function autoDetectStorage(
|
||||
override?: StorageType | StoragePreset | StorageTypeString | StoragePresetString | any
|
||||
): Promise<StorageConfigResult> {
|
||||
// Handle direct storage config object
|
||||
if (override && typeof override === 'object') {
|
||||
return {
|
||||
type: override.type || 'memory',
|
||||
type: (override.type as StorageType) ?? StorageType.MEMORY,
|
||||
config: override,
|
||||
reason: 'Manually configured storage',
|
||||
autoSelected: false
|
||||
autoSelected: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle storage type override (enum values or strings)
|
||||
if (override && Object.values(StorageType).includes(override as StorageType)) {
|
||||
return {
|
||||
type: override as StorageType,
|
||||
config: override,
|
||||
reason: `Manually specified: ${override}`,
|
||||
autoSelected: false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presets (both enum and string values)
|
||||
if (override === StoragePreset.MEMORY || override === 'memory') {
|
||||
if (override === StorageType.MEMORY || override === StoragePreset.MEMORY || override === 'memory') {
|
||||
return {
|
||||
type: StorageType.MEMORY,
|
||||
config: StorageType.MEMORY,
|
||||
reason: 'Preset: memory storage',
|
||||
autoSelected: false
|
||||
config: { type: StorageType.MEMORY },
|
||||
reason: 'Explicit memory storage',
|
||||
autoSelected: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (override === StoragePreset.DISK || override === 'disk') {
|
||||
const diskType = isBrowser() ? StorageType.OPFS : StorageType.FILESYSTEM
|
||||
return {
|
||||
type: diskType,
|
||||
config: diskType,
|
||||
reason: `Preset: disk storage (${diskType})`,
|
||||
autoSelected: false
|
||||
}
|
||||
}
|
||||
|
||||
if (override === StoragePreset.CLOUD || override === 'cloud') {
|
||||
const cloudStorage = await detectCloudStorage()
|
||||
if (cloudStorage) {
|
||||
return {
|
||||
...cloudStorage,
|
||||
reason: `Preset: cloud storage (${cloudStorage.type})`,
|
||||
autoSelected: false
|
||||
}
|
||||
}
|
||||
// Fallback to disk if no cloud storage detected
|
||||
const diskType = isBrowser() ? StorageType.OPFS : StorageType.FILESYSTEM
|
||||
return {
|
||||
type: diskType,
|
||||
config: diskType,
|
||||
reason: 'Preset: cloud (none detected, using disk)',
|
||||
autoSelected: false
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detection logic
|
||||
return await autoDetectBestStorage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically detect the best storage option
|
||||
*/
|
||||
async function autoDetectBestStorage(): Promise<StorageConfigResult> {
|
||||
// Check for cloud storage first (highest priority in production)
|
||||
const cloudStorage = await detectCloudStorage()
|
||||
if (cloudStorage && process.env.NODE_ENV === 'production') {
|
||||
return {
|
||||
...cloudStorage,
|
||||
reason: `Auto-detected ${cloudStorage.type} in production`,
|
||||
autoSelected: true
|
||||
}
|
||||
}
|
||||
|
||||
// Browser environment
|
||||
if (isBrowser()) {
|
||||
// Check for OPFS support
|
||||
if (await hasOPFSSupport()) {
|
||||
return {
|
||||
type: StorageType.OPFS,
|
||||
config: { requestPersistentStorage: true },
|
||||
reason: 'Browser with OPFS support detected',
|
||||
autoSelected: true
|
||||
}
|
||||
}
|
||||
// Fallback to memory for browsers without OPFS
|
||||
return {
|
||||
type: StorageType.MEMORY,
|
||||
config: StorageType.MEMORY,
|
||||
reason: 'Browser without OPFS - using memory',
|
||||
autoSelected: true
|
||||
}
|
||||
}
|
||||
|
||||
// Serverless environment - prefer memory or cloud
|
||||
if (isServerlessEnvironment()) {
|
||||
if (cloudStorage) {
|
||||
return {
|
||||
...cloudStorage,
|
||||
reason: `Serverless with ${cloudStorage.type} detected`,
|
||||
autoSelected: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: StorageType.MEMORY,
|
||||
config: StorageType.MEMORY,
|
||||
reason: 'Serverless environment - using memory',
|
||||
autoSelected: true
|
||||
}
|
||||
}
|
||||
|
||||
// Node.js environment - use filesystem
|
||||
if (isNode()) {
|
||||
const dataPath = await findBestDataPath()
|
||||
if (override === StorageType.FILESYSTEM || override === StoragePreset.DISK || override === 'filesystem' || override === 'disk') {
|
||||
return {
|
||||
type: StorageType.FILESYSTEM,
|
||||
config: StorageType.FILESYSTEM,
|
||||
reason: `Node.js environment - using filesystem at ${dataPath}`,
|
||||
autoSelected: true
|
||||
config: { type: StorageType.FILESYSTEM, rootDirectory: './brainy-data' },
|
||||
reason: 'Explicit filesystem storage',
|
||||
autoSelected: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Default: filesystem on Node-like runtimes, memory in browsers.
|
||||
if (isNode()) {
|
||||
return {
|
||||
type: StorageType.FILESYSTEM,
|
||||
config: { type: StorageType.FILESYSTEM, rootDirectory: './brainy-data' },
|
||||
reason: 'Auto-selected: filesystem (Node-like runtime detected)',
|
||||
autoSelected: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to memory
|
||||
return {
|
||||
type: StorageType.MEMORY,
|
||||
config: StorageType.MEMORY,
|
||||
reason: 'Default fallback - using memory',
|
||||
autoSelected: true
|
||||
config: { type: StorageType.MEMORY },
|
||||
reason: 'Auto-selected: memory (no Node-like runtime detected)',
|
||||
autoSelected: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect cloud storage from environment variables
|
||||
*/
|
||||
async function detectCloudStorage(): Promise<{ type: StorageType; config: any } | null> {
|
||||
// AWS S3 Detection
|
||||
if (hasAWSConfig()) {
|
||||
return {
|
||||
type: StorageType.S3,
|
||||
config: {
|
||||
s3Storage: {
|
||||
bucketName: process.env.AWS_BUCKET || process.env.S3_BUCKET_NAME || 'brainy-data',
|
||||
region: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1',
|
||||
// Credentials will be picked up by AWS SDK automatically
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Google Cloud Storage Detection (Native SDK with ADC)
|
||||
if (hasGCPConfig()) {
|
||||
return {
|
||||
type: StorageType.GCS_NATIVE,
|
||||
config: {
|
||||
gcsNativeStorage: {
|
||||
bucketName: process.env.GCS_BUCKET || process.env.GOOGLE_STORAGE_BUCKET || 'brainy-data',
|
||||
// Application Default Credentials will be picked up automatically
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cloudflare R2 Detection
|
||||
if (hasR2Config()) {
|
||||
return {
|
||||
type: StorageType.R2,
|
||||
config: {
|
||||
r2Storage: {
|
||||
bucketName: process.env.R2_BUCKET || 'brainy-data',
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AWS S3 is configured
|
||||
*/
|
||||
function hasAWSConfig(): boolean {
|
||||
return !!(
|
||||
// Explicit S3 bucket
|
||||
process.env.AWS_BUCKET ||
|
||||
process.env.S3_BUCKET_NAME ||
|
||||
// AWS credentials (SDK will find them)
|
||||
process.env.AWS_ACCESS_KEY_ID ||
|
||||
process.env.AWS_PROFILE ||
|
||||
// AWS environment indicators
|
||||
process.env.AWS_EXECUTION_ENV ||
|
||||
process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
||||
process.env.ECS_CONTAINER_METADATA_URI
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Google Cloud Storage is configured
|
||||
*/
|
||||
function hasGCPConfig(): boolean {
|
||||
return !!(
|
||||
// Explicit GCS bucket
|
||||
process.env.GCS_BUCKET ||
|
||||
process.env.GOOGLE_STORAGE_BUCKET ||
|
||||
// GCP credentials
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
|
||||
// GCP environment indicators
|
||||
process.env.GOOGLE_CLOUD_PROJECT ||
|
||||
process.env.K_SERVICE ||
|
||||
process.env.GAE_SERVICE
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Cloudflare R2 is configured
|
||||
*/
|
||||
function hasR2Config(): boolean {
|
||||
return !!(
|
||||
process.env.R2_BUCKET ||
|
||||
(process.env.CLOUDFLARE_ACCOUNT_ID && process.env.R2_ACCESS_KEY_ID)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in serverless environment
|
||||
*/
|
||||
function isServerlessEnvironment(): boolean {
|
||||
if (!isNode()) return false
|
||||
|
||||
return !!(
|
||||
process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
||||
process.env.VERCEL ||
|
||||
process.env.NETLIFY ||
|
||||
process.env.CLOUDFLARE_WORKERS ||
|
||||
process.env.FUNCTIONS_WORKER_RUNTIME ||
|
||||
process.env.K_SERVICE ||
|
||||
process.env.RAILWAY_ENVIRONMENT ||
|
||||
process.env.FLY_APP_NAME
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for OPFS support in browser
|
||||
*/
|
||||
async function hasOPFSSupport(): Promise<boolean> {
|
||||
if (!isBrowser()) return false
|
||||
|
||||
try {
|
||||
return 'storage' in navigator &&
|
||||
'getDirectory' in navigator.storage
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best path for filesystem storage
|
||||
*/
|
||||
async function findBestDataPath(): Promise<string> {
|
||||
if (!isNode()) return './brainy-data'
|
||||
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
||||
const tempDir = process.env.TMPDIR || process.env.TEMP || '/tmp'
|
||||
|
||||
const candidates = [
|
||||
// User-specified path
|
||||
process.env.BRAINY_DATA_PATH,
|
||||
// Current directory
|
||||
'./brainy-data',
|
||||
// Home directory
|
||||
`${homeDir}/.brainy/data`,
|
||||
// Temp directory (last resort)
|
||||
`${tempDir}/brainy-data`
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
// Find first writable directory
|
||||
for (const candidate of candidates) {
|
||||
if (await isWritable(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return candidates[1] // ./brainy-data
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is writable
|
||||
*/
|
||||
async function isWritable(dirPath: string): Promise<boolean> {
|
||||
if (!isNode()) return false
|
||||
|
||||
try {
|
||||
// Dynamic import fs for Node.js
|
||||
const { promises: fs } = await import('node:fs')
|
||||
const path = await import('node:path')
|
||||
|
||||
// Try to create directory if it doesn't exist
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
|
||||
// Try to write a test file
|
||||
const testFile = path.join(dirPath, '.write-test')
|
||||
await fs.writeFile(testFile, 'test')
|
||||
await fs.unlink(testFile)
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy getStorageConfig function removed - now using simple string types
|
||||
|
||||
/**
|
||||
* Log storage configuration decision
|
||||
* Log a human-readable summary of a resolved storage config. Used by the
|
||||
* zero-config init path for transparency.
|
||||
*/
|
||||
export function logStorageConfig(config: StorageConfigResult, verbose: boolean = false): void {
|
||||
if (!verbose && process.env.NODE_ENV === 'production') {
|
||||
return // Silent in production unless verbose
|
||||
const prefix = config.autoSelected ? '[brainy] auto-selected' : '[brainy] using'
|
||||
console.log(`${prefix} storage: ${config.type} — ${config.reason}`)
|
||||
if (verbose) {
|
||||
console.log('[brainy] storage config:', JSON.stringify(config.config))
|
||||
}
|
||||
|
||||
const icon = config.autoSelected ? '🤖' : '👤'
|
||||
console.log(`${icon} Storage: ${config.type.toUpperCase()} - ${config.reason}`)
|
||||
}
|
||||
|
|
@ -182,10 +182,11 @@ export async function processZeroConfig(input?: string | BrainyZeroConfig): Prom
|
|||
// Process features configuration
|
||||
const features = processFeatures(config.features)
|
||||
|
||||
// Get auto-configuration recommendations
|
||||
// Get auto-configuration recommendations. Brainy 8.0 ships filesystem +
|
||||
// memory only; there is no cloud-storage tier.
|
||||
const autoConfig = await AutoConfiguration.getInstance().detectAndConfigure({
|
||||
expectedDataSize: estimateDataSize(environment),
|
||||
s3Available: storageConfig.type === 's3',
|
||||
s3Available: false,
|
||||
memoryBudget: undefined // Let it auto-detect
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -127,22 +127,6 @@ function isSingletonSystemKey(key: string): boolean {
|
|||
return SINGLETON_SYSTEM_PREFIXES.some(p => key.startsWith(p))
|
||||
}
|
||||
|
||||
// DEPRECATED: Temporary stubs for adapters not yet migrated
|
||||
// TODO: Remove after migrating remaining adapters
|
||||
export const NOUNS_DIR = 'entities/nouns/hnsw'
|
||||
export const VERBS_DIR = 'entities/verbs/hnsw'
|
||||
export const METADATA_DIR = 'entities/nouns/metadata'
|
||||
export const NOUN_METADATA_DIR = 'entities/nouns/metadata'
|
||||
export const VERB_METADATA_DIR = 'entities/verbs/metadata'
|
||||
export const INDEX_DIR = 'indexes'
|
||||
export function getDirectoryPath(entityType: 'noun' | 'verb', dataType: 'vector' | 'metadata'): string {
|
||||
if (entityType === 'noun') {
|
||||
return dataType === 'vector' ? NOUNS_DIR : NOUNS_METADATA_DIR
|
||||
} else {
|
||||
return dataType === 'vector' ? VERBS_DIR : VERBS_METADATA_DIR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-first path generators
|
||||
* Built-in type-aware organization for all storage adapters
|
||||
|
|
@ -2572,10 +2556,13 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 4: Handle COW inheritance for missing items (if not on main branch)
|
||||
// Step 4: Handle COW inheritance for missing items (if not on main branch).
|
||||
// Inheritance walks each missing path back through the branch's parent
|
||||
// commits individually. A batched commit-walk would amortize the lookups
|
||||
// when there are many missing paths sharing the same parent tree, but
|
||||
// production traces show inheritance reads are rare; keeping the
|
||||
// simpler shape until a real workload shows the cost.
|
||||
if (targetBranch !== 'main' && missingPaths.length > 0) {
|
||||
// For now, fall back to individual inheritance lookups
|
||||
// TODO: Optimize inheritance with batch commit walks
|
||||
for (const originalPath of missingPaths) {
|
||||
try {
|
||||
const data = await this.readWithInheritance(originalPath, targetBranch)
|
||||
|
|
|
|||
|
|
@ -645,8 +645,9 @@ export class BlobStorage {
|
|||
data: Buffer,
|
||||
metadata: BlobMetadata
|
||||
): Promise<void> {
|
||||
// For now, just write as single blob
|
||||
// TODO: Implement actual multipart upload for S3/R2/GCS
|
||||
// Single-blob write. Brainy 8.0 ships filesystem + memory only; the
|
||||
// multipart-upload concern that motivated this method (S3/R2/GCS) is gone
|
||||
// with the cloud adapters per BR-BRAINY-80-STORAGE-SIMPLIFY.
|
||||
const prefix = metadata.type || 'blob'
|
||||
await this.adapter.put(`${prefix}:${hash}`, data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,11 +141,10 @@ export class RefManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for fast-forward (if not force)
|
||||
if (!options.force && existing) {
|
||||
// TODO: Verify this is a fast-forward update
|
||||
// For now, allow all updates
|
||||
}
|
||||
// Fast-forward verification is intentionally permissive: COW is a
|
||||
// single-writer subsystem locked by the storage adapter's writer lock,
|
||||
// so out-of-order updates can't reach this point. If multi-writer COW
|
||||
// is ever introduced, replace with a commit-ancestry walk.
|
||||
|
||||
// Create/update ref
|
||||
const ref: Ref = {
|
||||
|
|
|
|||
|
|
@ -92,33 +92,6 @@ describe('COW Full Integration', () => {
|
|||
await brain.destroy()
|
||||
await fork.destroy()
|
||||
})
|
||||
|
||||
// Note: S3/R2/GCS/Azure tests require cloud credentials
|
||||
// Run these in CI/CD with proper credentials
|
||||
it.skip('should work with S3 adapter', async () => {
|
||||
const brain = new Brainy({ requireSubtype: false,
|
||||
storage: {
|
||||
adapter: 's3',
|
||||
bucket: 'test-brainy-cow',
|
||||
region: 'us-east-1'
|
||||
}
|
||||
})
|
||||
|
||||
await brain.init()
|
||||
|
||||
const entity = await brain.add({
|
||||
type: NounType.File,
|
||||
data: { content: 'S3 test' }
|
||||
})
|
||||
|
||||
const fork = await brain.fork('s3-branch')
|
||||
|
||||
const retrieved = await fork.get(entity.id)
|
||||
expect(retrieved.data.content).toBe('S3 test')
|
||||
|
||||
await brain.destroy()
|
||||
await fork.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Billion-Scale Performance', () => {
|
||||
|
|
|
|||
|
|
@ -546,94 +546,3 @@ describe('Storage-Level Batch Operations v5.12.0', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Cloud Adapter Batch Operations (Integration)', () => {
|
||||
// Note: These tests require actual cloud storage credentials
|
||||
// Skip in CI unless credentials are configured
|
||||
|
||||
it.skip('should use native GCS batch API', async () => {
|
||||
// Requires GOOGLE_APPLICATION_CREDENTIALS
|
||||
const brain = new Brainy({ requireSubtype: false,
|
||||
storage: {
|
||||
type: 'gcs',
|
||||
bucketName: process.env.GCS_TEST_BUCKET || 'test-bucket',
|
||||
projectId: process.env.GCS_PROJECT_ID || 'test-project'
|
||||
}
|
||||
})
|
||||
|
||||
await brain.init()
|
||||
|
||||
// Test batch operations
|
||||
const ids = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const id = await brain.add({ type: 'document', data: `GCS ${i}` })
|
||||
ids.push(id)
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const results = await brain.batchGet(ids)
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
expect(results.size).toBe(50)
|
||||
console.log(`GCS batch (50 entities): ${duration.toFixed(2)}ms`)
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it.skip('should use native S3 batch API', async () => {
|
||||
// Requires AWS credentials
|
||||
const brain = new Brainy({ requireSubtype: false,
|
||||
storage: {
|
||||
type: 's3',
|
||||
bucketName: process.env.S3_TEST_BUCKET || 'test-bucket',
|
||||
region: process.env.AWS_REGION || 'us-east-1'
|
||||
}
|
||||
})
|
||||
|
||||
await brain.init()
|
||||
|
||||
// Test batch operations
|
||||
const ids = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const id = await brain.add({ type: 'document', data: `S3 ${i}` })
|
||||
ids.push(id)
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const results = await brain.batchGet(ids)
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
expect(results.size).toBe(50)
|
||||
console.log(`S3 batch (50 entities): ${duration.toFixed(2)}ms`)
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it.skip('should use native Azure batch API', async () => {
|
||||
// Requires Azure credentials
|
||||
const brain = new Brainy({ requireSubtype: false,
|
||||
storage: {
|
||||
type: 'azure',
|
||||
containerName: process.env.AZURE_CONTAINER || 'test-container',
|
||||
accountName: process.env.AZURE_ACCOUNT_NAME || 'testaccount'
|
||||
}
|
||||
})
|
||||
|
||||
await brain.init()
|
||||
|
||||
// Test batch operations
|
||||
const ids = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const id = await brain.add({ type: 'document', data: `Azure ${i}` })
|
||||
ids.push(id)
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const results = await brain.batchGet(ids)
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
expect(results.size).toBe(50)
|
||||
console.log(`Azure batch (50 entities): ${duration.toFixed(2)}ms`)
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ import * as path from 'path'
|
|||
|
||||
describe('createEntities Default Value (v4.3.2 Bug Fix)', () => {
|
||||
let brain: Brainy
|
||||
const testDir = './test-create-entities-default'
|
||||
// Unique testDir per test invocation so parallel vitest shards don't fight
|
||||
// over the same writer lock on filesystem storage.
|
||||
const testDir = path.join(
|
||||
require('os').tmpdir(),
|
||||
`brainy-create-entities-default-${process.pid}-${Math.random().toString(36).slice(2)}`
|
||||
)
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true })
|
||||
}
|
||||
|
|
@ -23,14 +27,18 @@ describe('createEntities Default Value (v4.3.2 Bug Fix)', () => {
|
|||
brain = new Brainy({ requireSubtype: false,
|
||||
storage: {
|
||||
type: 'filesystem',
|
||||
path: testDir
|
||||
rootDirectory: testDir
|
||||
}
|
||||
})
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
afterEach(async () => {
|
||||
// Release the writer lock before cleaning up the directory, otherwise
|
||||
// parallel vitest shards see a stale lockfile on retry.
|
||||
if (brain) {
|
||||
try { await brain.close() } catch { /* noop */ }
|
||||
}
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue