brainy/tests/helpers/TestWrappingAdapter.ts
David Snelling 9a8b7a6cd4 fix: critical blob integrity regression with defense-in-depth architecture (v5.10.1)
CRITICAL BUG FIX: v5.10.0 regressed the v5.7.2 blob integrity bug, causing
100% VFS file read failure. This fix restores functionality with production-grade
defense-in-depth architecture and comprehensive testing.

Problem:
- v5.10.0 reintroduced bug where BlobStorage.read() hashed wrapped data
- Symptom: "Blob integrity check failed" on every VFS file read
- Impact: 100% failure rate in Workshop application
- Root Cause: Missing defense-in-depth unwrap verification

The Fix:
1. Defense-in-Depth Unwrapping
   - Added unwrap verification in BlobStorage.read() before hash check (line 342)
   - Added unwrap for metadata parsing (line 314)
   - Ensures data is always unwrapped regardless of adapter behavior

2. DRY Architecture
   - Created binaryDataCodec.ts as single source of truth
   - Refactored baseStorage to use shared utilities
   - All 8 storage adapters now use same implementation

3. Comprehensive Testing
   - Added TestWrappingAdapter that actually wraps like production
   - 3 new regression tests validate the fix
   - Tests exercise real wrapping scenario that caused the bug

Architecture Improvements:
-  Defense-in-Depth: Unwrap at BOTH adapter and blob layers
-  DRY Principle: Single source of truth in binaryDataCodec.ts
-  Works Across ALL Storage Adapters (8 total)
-  Prevents Future Regressions: Real wrapping tests

Files Changed:
- NEW: src/storage/cow/binaryDataCodec.ts (single source of truth)
- FIXED: src/storage/cow/BlobStorage.ts (defense-in-depth unwrap)
- REFACTORED: src/storage/baseStorage.ts (uses shared codec)
- NEW: tests/helpers/TestWrappingAdapter.ts (real wrapping adapter)
- ADDED: 3 regression tests in tests/unit/storage/cow/BlobStorage.test.ts
- UPDATED: CHANGELOG.md, package.json (v5.10.1)

Related Issues:
- v5.7.2: Original bug - hashed wrapper instead of content
- v5.7.5: First fix - added unwrap to adapter (necessary but insufficient)
- v5.10.0: Regression - missing defense-in-depth in BlobStorage
- v5.10.1: Complete fix - defense-in-depth + DRY + tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:31:06 -08:00

92 lines
2.3 KiB
TypeScript

/**
* TestWrappingAdapter: Real COW storage adapter for testing
*
* This adapter ACTUALLY wraps binary data in JSON (like production storage)
* to properly test the unwrap logic in BlobStorage.
*
* Unlike InMemoryCOWAdapter which stores Buffers directly,
* this adapter simulates real storage behavior:
* 1. Compresses with gzip
* 2. Wraps binary as {_binary: true, data: "base64..."}
* 3. Parses JSON on read (returns JS object, not Buffer)
*
* This ensures tests exercise the ACTUAL code path that caused v5.10.0 regression.
*/
import { COWStorageAdapter } from '../../src/storage/cow/BlobStorage.js'
import { gzip, gunzip } from 'zlib'
import { promisify } from 'util'
const gzipAsync = promisify(gzip)
const gunzipAsync = promisify(gunzip)
export class TestWrappingAdapter implements COWStorageAdapter {
private storage = new Map<string, Buffer>()
async get(key: string): Promise<any | undefined> {
const compressed = this.storage.get(key)
if (!compressed) {
return undefined
}
// Decompress (like real storage)
const decompressed = await gunzipAsync(compressed)
// Parse JSON (like real storage)
// This returns a JS object: {_binary: true, data: "base64..."}
// NOT a Buffer!
return JSON.parse(decompressed.toString())
}
async put(key: string, data: Buffer): Promise<void> {
// Determine if data is JSON or binary
let obj: any
try {
// Try to parse as JSON (for metadata)
obj = JSON.parse(data.toString())
} catch {
// Not JSON - wrap as binary (like production)
obj = {
_binary: true,
data: data.toString('base64')
}
}
// Stringify to JSON
const jsonStr = JSON.stringify(obj)
// Compress (like real storage)
const compressed = await gzipAsync(Buffer.from(jsonStr))
// Store compressed data
this.storage.set(key, compressed)
}
async delete(key: string): Promise<void> {
this.storage.delete(key)
}
async list(prefix: string): Promise<string[]> {
const keys: string[] = []
for (const key of this.storage.keys()) {
if (key.startsWith(prefix)) {
keys.push(key)
}
}
return keys
}
/**
* Clear all storage (for test cleanup)
*/
clear(): void {
this.storage.clear()
}
/**
* Get storage size (for testing)
*/
size(): number {
return this.storage.size
}
}