brainy/tests/transaction/Transaction.test.ts
David Snelling e40fee39d8 feat: add v5.8.0 features - transactions, pagination, and comprehensive docs
**Transaction System (TIER 1.2)**
- Atomic operations with automatic rollback
- 36 unit tests + 35 integration tests passing
- Full documentation in docs/transactions.md

**Duplicate Check Optimization (TIER 1.4)**
- Optimized from O(n) to O(log n) using GraphAdjacencyIndex
- Uses LSM-tree for efficient lookups
- Tests verify performance improvements

**GraphIndex Pagination (TIER 1.5)**
- Production-scale pagination for high-degree nodes
- Backward compatible API
- 18 pagination tests passing

**Comprehensive Filter Documentation (TIER 1.6)**
- Complete operator reference (15 operators)
- Compound filters (anyOf, allOf, nested logic)
- Common query patterns and troubleshooting guide
- 642 lines of new documentation

**README Updates**
- Added Filter & Query Syntax Guide to Essential Reading
- Added Transactions to Core Concepts section

All changes tested and production-ready for v5.8.0 release.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 10:26:23 -08:00

396 lines
10 KiB
TypeScript

/**
* Transaction Core Tests
*
* Tests the Transaction class for:
* - Basic execution flow
* - Rollback on failure
* - State management
* - Timeout handling
* - Retry logic
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Transaction } from '../../src/transaction/Transaction.js'
import type { Operation, RollbackAction } from '../../src/transaction/types.js'
import {
InvalidTransactionStateError,
TransactionExecutionError,
TransactionTimeoutError
} from '../../src/transaction/errors.js'
describe('Transaction', () => {
describe('Basic Execution', () => {
it('should execute operations in order and commit', async () => {
const executionOrder: number[] = []
const op1: Operation = {
name: 'Operation1',
execute: async () => {
executionOrder.push(1)
return async () => { /* rollback */ }
}
}
const op2: Operation = {
name: 'Operation2',
execute: async () => {
executionOrder.push(2)
return async () => { /* rollback */ }
}
}
const tx = new Transaction()
tx.addOperation(op1)
tx.addOperation(op2)
await tx.execute()
expect(executionOrder).toEqual([1, 2])
expect(tx.getState()).toBe('committed')
})
it('should track operation count', () => {
const tx = new Transaction()
expect(tx.getOperationCount()).toBe(0)
tx.addOperation({ execute: async () => undefined })
expect(tx.getOperationCount()).toBe(1)
tx.addOperation({ execute: async () => undefined })
expect(tx.getOperationCount()).toBe(2)
})
it('should record execution time', async () => {
const tx = new Transaction()
tx.addOperation({
execute: async () => {
// Small delay to ensure measurable time
await new Promise(resolve => setTimeout(resolve, 1))
return async () => {}
}
})
await tx.execute()
const executionTime = tx.getExecutionTimeMs()
expect(executionTime).toBeGreaterThanOrEqual(0) // Can be 0 for very fast operations
expect(executionTime).toBeLessThan(1000) // Should be reasonably quick
})
})
describe('Rollback on Failure', () => {
it('should rollback all operations when one fails', async () => {
const rollbackOrder: number[] = []
const op1: Operation = {
name: 'Operation1',
execute: async () => {
return async () => { rollbackOrder.push(1) }
}
}
const op2: Operation = {
name: 'Operation2',
execute: async () => {
throw new Error('Operation 2 failed')
}
}
const op3: Operation = {
name: 'Operation3',
execute: async () => {
return async () => { rollbackOrder.push(3) }
}
}
const tx = new Transaction()
tx.addOperation(op1)
tx.addOperation(op2)
tx.addOperation(op3)
await expect(tx.execute()).rejects.toThrow(TransactionExecutionError)
expect(tx.getState()).toBe('rolled_back')
// Only op1 executed before failure, so only op1 should rollback
expect(rollbackOrder).toEqual([1])
})
it('should rollback operations in reverse order', async () => {
const rollbackOrder: number[] = []
const op1: Operation = {
execute: async () => {
return async () => { rollbackOrder.push(1) }
}
}
const op2: Operation = {
execute: async () => {
return async () => { rollbackOrder.push(2) }
}
}
const op3: Operation = {
execute: async () => {
throw new Error('op3 failed')
}
}
const tx = new Transaction()
tx.addOperation(op1)
tx.addOperation(op2)
tx.addOperation(op3)
await expect(tx.execute()).rejects.toThrow()
// Rollback in reverse order: op2, then op1
expect(rollbackOrder).toEqual([2, 1])
})
it('should retry failed rollback operations', async () => {
let rollbackAttempts = 0
const op: Operation = {
execute: async () => {
return async () => {
rollbackAttempts++
if (rollbackAttempts < 2) {
throw new Error('Rollback failed')
}
// Success on 2nd attempt
}
}
}
const failingOp: Operation = {
execute: async () => {
throw new Error('Operation failed')
}
}
const tx = new Transaction()
tx.addOperation(op)
tx.addOperation(failingOp)
await expect(tx.execute()).rejects.toThrow()
// Should have retried rollback
expect(rollbackAttempts).toBe(2)
})
it('should continue rollback even if one rollback fails', async () => {
const rollbackOrder: number[] = []
const op1: Operation = {
execute: async () => {
return async () => { rollbackOrder.push(1) }
}
}
const op2: Operation = {
execute: async () => {
return async () => {
rollbackOrder.push(2)
throw new Error('Rollback 2 failed')
}
}
}
const op3: Operation = {
execute: async () => {
throw new Error('op3 failed')
}
}
const tx = new Transaction({ maxRollbackRetries: 1 })
tx.addOperation(op1)
tx.addOperation(op2)
tx.addOperation(op3)
await expect(tx.execute()).rejects.toThrow()
// Should attempt both rollbacks despite op2 failing
expect(rollbackOrder).toContain(1)
expect(rollbackOrder).toContain(2)
})
})
describe('State Management', () => {
it('should prevent adding operations after execution starts', async () => {
const tx = new Transaction()
tx.addOperation({ execute: async () => undefined })
const executePromise = tx.execute()
expect(() => {
tx.addOperation({ execute: async () => undefined })
}).toThrow(InvalidTransactionStateError)
await executePromise
})
it('should prevent double execution', async () => {
const tx = new Transaction()
tx.addOperation({ execute: async () => undefined })
await tx.execute()
await expect(tx.execute()).rejects.toThrow(InvalidTransactionStateError)
})
it('should transition through states correctly', async () => {
const states: string[] = []
const tx = new Transaction()
states.push(tx.getState())
tx.addOperation({
execute: async () => {
states.push(tx.getState())
return async () => {}
}
})
await tx.execute()
states.push(tx.getState())
expect(states).toEqual(['pending', 'executing', 'committed'])
})
})
describe('Timeout Handling', () => {
it('should timeout long-running transactions with many operations', async () => {
const tx = new Transaction({ timeout: 50 })
// Add many operations that cumulatively exceed timeout
for (let i = 0; i < 10; i++) {
tx.addOperation({
execute: async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return async () => {}
}
})
}
await expect(tx.execute()).rejects.toThrow(TransactionTimeoutError)
})
it('should not timeout fast transactions', async () => {
const tx = new Transaction({ timeout: 1000 })
tx.addOperation({
execute: async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return async () => {}
}
})
await expect(tx.execute()).resolves.toBeUndefined()
})
})
describe('Options', () => {
it('should respect custom timeout', async () => {
const tx = new Transaction({ timeout: 50 })
// Many operations to ensure cumulative time exceeds timeout
for (let i = 0; i < 10; i++) {
tx.addOperation({
execute: async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return async () => {}
}
})
}
const error = await tx.execute().catch(e => e)
expect(error).toBeInstanceOf(TransactionTimeoutError)
})
it('should respect maxRollbackRetries', async () => {
let attempts = 0
const tx = new Transaction({ maxRollbackRetries: 2 })
tx.addOperation({
execute: async () => {
return async () => {
attempts++
throw new Error('Rollback failed')
}
}
})
tx.addOperation({
execute: async () => {
throw new Error('Operation failed')
}
})
await expect(tx.execute()).rejects.toThrow()
// Should try maxRollbackRetries times
expect(attempts).toBe(2)
})
})
describe('Error Context', () => {
it('should include operation name in error', async () => {
const tx = new Transaction()
tx.addOperation({
name: 'MySpecialOperation',
execute: async () => {
throw new Error('Failed')
}
})
const error = await tx.execute().catch(e => e)
expect(error).toBeInstanceOf(TransactionExecutionError)
expect(error.operationName).toBe('MySpecialOperation')
})
it('should include operation index in error', async () => {
const tx = new Transaction()
tx.addOperation({ execute: async () => undefined })
tx.addOperation({ execute: async () => undefined })
tx.addOperation({
execute: async () => {
throw new Error('Failed')
}
})
const error = await tx.execute().catch(e => e)
expect(error).toBeInstanceOf(TransactionExecutionError)
expect(error.operationIndex).toBe(2)
})
})
describe('Empty Transactions', () => {
it('should handle empty transaction', async () => {
const tx = new Transaction()
await tx.execute()
expect(tx.getState()).toBe('committed')
expect(tx.getOperationCount()).toBe(0)
})
})
describe('Operations without Rollback', () => {
it('should handle operations that return no rollback action', async () => {
const tx = new Transaction()
tx.addOperation({
execute: async () => {
return undefined // No rollback needed
}
})
await expect(tx.execute()).resolves.toBeUndefined()
expect(tx.getState()).toBe('committed')
})
})
})