feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API
The COW version-control surface (fork, branches, checkout, commit,
getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with
its subsystems: src/versioning/, the COW object store (CommitLog,
CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the
TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/
with/persist/restore) is the one versioning model in 8.0.
Survivors and replacements:
- BlobStorage survives (the VFS stores file content through it), relocated
to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter
interface is now BlobStoreAdapter, slimmed to the consumed surface
(write/read/has/delete/getMetadata + MIME-aware compression policy).
- brain.migrate() backup branches are replaced by persist-before-migrate:
MigrateOptions.backupTo persists a hard-link snapshot of the current
generation before any transform runs; MigrationResult.backupPath reports
it, and brain.restore(path) brings it back wholesale.
- CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by
snapshot.ts — snapshot <path>, restore <path>, history (tx-log),
generation.
- New public read API: brain.transactionLog({limit}) exposes the reified
tx-log (generation/timestamp/meta, newest first) that backs the CLI
history command; TxLogEntry is exported.
Tests: superseded suites deleted; fork/commit blocks excised from shared
suites; BlobStorage tests relocated + reworked against the slimmed store;
migration tests now prove the backupTo snapshot/restore round trip; new
transactionLog coverage in db-mvcc.
This commit is contained in:
parent
431cd64406
commit
8f93add705
64 changed files with 1444 additions and 16313 deletions
|
|
@ -4,7 +4,7 @@
|
|||
* Verifies:
|
||||
* - MigrationRunner with in-memory storage
|
||||
* - brain.migrate() public API (dry-run and apply)
|
||||
* - Backup branch creation with metadata tagging
|
||||
* - Pre-migration snapshots via `backupTo` (persist-before-migrate + restore)
|
||||
* - No-op when MIGRATIONS array is empty
|
||||
* - Warning log when autoMigrate is false
|
||||
* - Multi-tenant independent migration state
|
||||
|
|
@ -12,6 +12,9 @@
|
|||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import * as fs from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { Brainy } from '../../src/brainy.js'
|
||||
import { MigrationRunner, MIGRATIONS } from '../../src/migration/index.js'
|
||||
import type { Migration } from '../../src/migration/index.js'
|
||||
|
|
@ -242,13 +245,21 @@ describe('Migration System', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// ─── Backup branch tests ──────────────────────────────────────
|
||||
// ─── Pre-migration snapshot tests ─────────────────────────────
|
||||
|
||||
describe('Backup branches', () => {
|
||||
it('should create a backup branch before migrating', async () => {
|
||||
describe('Pre-migration snapshots (backupTo)', () => {
|
||||
let backupDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
backupDir = path.join(os.tmpdir(), `brainy-migration-backup-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(backupDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('should persist a snapshot before migrating and report its path', async () => {
|
||||
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { x: 1 } })
|
||||
// Need a commit for fork to work
|
||||
await brain.commit({ message: 'test', author: 'test' })
|
||||
|
||||
const migration: Migration = {
|
||||
id: 'test-backup',
|
||||
|
|
@ -259,45 +270,19 @@ describe('Migration System', () => {
|
|||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
const result = await brain.migrate()
|
||||
const result = await brain.migrate({ backupTo: backupDir })
|
||||
const r = result as any
|
||||
expect(r.backupBranch).toBe('pre-migration-2.0.0')
|
||||
expect(r.backupPath).toBe(backupDir)
|
||||
expect(r.migrationsApplied).toContain('test-backup')
|
||||
|
||||
const branches = await brain.listBranches()
|
||||
expect(branches).toContain('pre-migration-2.0.0')
|
||||
// The snapshot is a self-contained store directory
|
||||
expect(fs.existsSync(backupDir)).toBe(true)
|
||||
expect(fs.readdirSync(backupDir).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should tag backup branch with system:backup metadata', async () => {
|
||||
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { z: 1 } })
|
||||
await brain.commit({ message: 'test', author: 'test' })
|
||||
|
||||
const migration: Migration = {
|
||||
id: 'test-tag',
|
||||
version: '4.0.0',
|
||||
description: 'Tag test',
|
||||
applies: 'nouns',
|
||||
transform: (m) => 'z' in m ? { ...m, tagged: true } : null
|
||||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
await brain.migrate()
|
||||
|
||||
// Verify metadata tag on the backup branch ref
|
||||
const refManager = (brain as any).storage.refManager
|
||||
if (refManager) {
|
||||
const ref = await refManager.getRef('pre-migration-4.0.0')
|
||||
expect(ref).toBeDefined()
|
||||
expect(ref?.metadata?.type).toBe('system:backup')
|
||||
expect(ref?.metadata?.migrationVersion).toBe('4.0.0')
|
||||
expect(ref?.metadata?.author).toBe('brainy-migration')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep backup branch as a named restore point', async () => {
|
||||
it('should restore the pre-migration state wholesale from the snapshot', async () => {
|
||||
const id = await brain.add({ type: NounType.Concept, data: { name: 'Restore Test' }, metadata: { original: true } })
|
||||
await brain.commit({ message: 'before migration', author: 'test' })
|
||||
|
||||
const migration: Migration = {
|
||||
id: 'test-restore',
|
||||
|
|
@ -313,29 +298,39 @@ describe('Migration System', () => {
|
|||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
const result = await brain.migrate()
|
||||
const result = await brain.migrate({ backupTo: backupDir })
|
||||
const r = result as any
|
||||
expect(r.backupPath).toBe(backupDir)
|
||||
|
||||
// Verify migration was applied
|
||||
const migrated = await brain.get(id)
|
||||
expect(migrated?.metadata?.migrated).toBe(true)
|
||||
expect(migrated?.metadata?.original).toBe(false)
|
||||
|
||||
// Verify backup branch exists as a named restore point
|
||||
expect(r.backupBranch).toBe('pre-migration-3.0.0')
|
||||
const branches = await brain.listBranches()
|
||||
expect(branches).toContain('pre-migration-3.0.0')
|
||||
// Restore the snapshot — the store returns to its pre-migration state
|
||||
await brain.restore(backupDir, { confirm: true })
|
||||
const restored = await brain.get(id)
|
||||
expect(restored?.metadata?.original).toBe(true)
|
||||
expect(restored?.metadata?.migrated).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// Verify the backup ref points to the pre-migration commit
|
||||
const refManager = (brain as any).storage.refManager
|
||||
if (refManager) {
|
||||
const mainRef = await refManager.getRef('main')
|
||||
const backupRef = await refManager.getRef('pre-migration-3.0.0')
|
||||
expect(backupRef).toBeDefined()
|
||||
// Backup was forked before migration, so both share the same commit
|
||||
// (the pre-migration commit). After migration, main's overlay has new data,
|
||||
// but the commit pointer is unchanged.
|
||||
expect(backupRef.commitHash).toBe(mainRef.commitHash)
|
||||
}
|
||||
it('should report backupPath null when no backupTo is supplied', async () => {
|
||||
await brain.add({ type: NounType.Concept, data: { name: 'NoBackup' }, metadata: { q: 1 } })
|
||||
|
||||
const migration: Migration = {
|
||||
id: 'test-no-backup',
|
||||
version: '4.0.0',
|
||||
description: 'Add field',
|
||||
applies: 'nouns',
|
||||
transform: (m) => 'q' in m && !('r' in m) ? { ...m, r: 2 } : null
|
||||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
const result = await brain.migrate()
|
||||
const r = result as any
|
||||
expect(r.backupPath).toBeNull()
|
||||
expect(r.migrationsApplied).toContain('test-no-backup')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -520,86 +515,6 @@ describe('Migration System', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// ─── Multi-branch migration ─────────────────────────────────
|
||||
|
||||
describe('Multi-branch migration', () => {
|
||||
it('should migrate entities on all branches including feature branches', async () => {
|
||||
// Setup: create entities on main, fork a branch, add entities on the branch
|
||||
const mainId = await brain.add({
|
||||
type: NounType.Concept,
|
||||
data: { name: 'Main Entity' },
|
||||
metadata: { legacy: true, source: 'main' }
|
||||
})
|
||||
await brain.commit({ message: 'initial', author: 'test' })
|
||||
|
||||
// Fork a feature branch and add branch-local entity
|
||||
const fork = await brain.fork('feature-x')
|
||||
const branchId = await fork.add({
|
||||
type: NounType.Concept,
|
||||
data: { name: 'Branch Entity' },
|
||||
metadata: { legacy: true, source: 'branch' }
|
||||
})
|
||||
|
||||
// Define migration that transforms the 'legacy' field
|
||||
const migration: Migration = {
|
||||
id: 'test-multi-branch',
|
||||
version: '1.0.0',
|
||||
description: 'Mark legacy entities as upgraded',
|
||||
applies: 'nouns',
|
||||
transform: (m) => {
|
||||
if (m.legacy === true) {
|
||||
return { ...m, legacy: false, upgraded: true }
|
||||
}
|
||||
return null // Already migrated or not applicable
|
||||
}
|
||||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
// Migrate from main — should reach all branches
|
||||
const result = await brain.migrate()
|
||||
const r = result as any
|
||||
|
||||
expect(r.migrationsApplied).toContain('test-multi-branch')
|
||||
// Main entity + branch-local entity should both be modified
|
||||
expect(r.entitiesModified).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// Verify main entity was transformed
|
||||
const mainEntity = await brain.get(mainId)
|
||||
expect(mainEntity?.metadata?.upgraded).toBe(true)
|
||||
expect(mainEntity?.metadata?.legacy).toBe(false)
|
||||
})
|
||||
|
||||
await fork.close()
|
||||
})
|
||||
|
||||
it('should skip system:backup branches during multi-branch migration', async () => {
|
||||
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { v: 1 } })
|
||||
await brain.commit({ message: 'test', author: 'test' })
|
||||
|
||||
// Create two migrations — first creates a backup branch, second should skip it
|
||||
const migration1: Migration = {
|
||||
id: 'test-skip-backup-1',
|
||||
version: '5.0.0',
|
||||
description: 'First migration',
|
||||
applies: 'nouns',
|
||||
transform: (m) => typeof m.v === 'number' && !('m1' in m) ? { ...m, m1: true } : null
|
||||
}
|
||||
|
||||
await withMigrations([migration1], async () => {
|
||||
const result = await brain.migrate()
|
||||
const r = result as any
|
||||
expect(r.backupBranch).toBe('pre-migration-5.0.0')
|
||||
|
||||
// Verify backup branch exists
|
||||
const branches = await brain.listBranches()
|
||||
expect(branches).toContain('pre-migration-5.0.0')
|
||||
|
||||
// Migration should not have errored from trying to migrate the backup
|
||||
expect(r.migrationsApplied).toContain('test-skip-backup-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ─── MigrationRunner unit-level tests ─────────────────────────
|
||||
|
||||
describe('MigrationRunner', () => {
|
||||
|
|
@ -684,47 +599,6 @@ describe('Migration System', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should propagate errors from branch migrations into the result', async () => {
|
||||
// Create entity on main, commit, fork a branch, add a branch-local entity that will fail
|
||||
await brain.add({ type: NounType.Concept, data: { name: 'MainOk' }, metadata: { branchTest: 1 } })
|
||||
await brain.commit({ message: 'initial', author: 'test' })
|
||||
|
||||
const fork = await brain.fork('branch-with-error')
|
||||
await fork.add({ type: NounType.Concept, data: { name: 'BranchBad' }, metadata: { branchTest: 'crash' } })
|
||||
|
||||
const migration: Migration = {
|
||||
id: 'test-branch-error-prop',
|
||||
version: '1.0.0',
|
||||
description: 'Throws on non-number branchTest',
|
||||
applies: 'nouns',
|
||||
transform: (m) => {
|
||||
if ('branchTest' in m) {
|
||||
if (typeof m.branchTest !== 'number') {
|
||||
throw new Error('branchTest must be a number')
|
||||
}
|
||||
return { ...m, branchTest: (m.branchTest as number) * 10 }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
await withMigrations([migration], async () => {
|
||||
const result = await brain.migrate()
|
||||
const r = result as any
|
||||
|
||||
// Main entity should have migrated successfully
|
||||
expect(r.migrationsApplied).toContain('test-branch-error-prop')
|
||||
expect(r.entitiesModified).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Branch error should appear in the combined result
|
||||
expect(r.errors.length).toBeGreaterThanOrEqual(1)
|
||||
const branchError = r.errors.find((e: any) => e.error.includes('branchTest must be a number'))
|
||||
expect(branchError).toBeDefined()
|
||||
})
|
||||
|
||||
await fork.close()
|
||||
})
|
||||
|
||||
it('should stop early when maxErrors is exceeded', async () => {
|
||||
// Add many entities that will all fail
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue