27 test files matched no vitest config, so they never ran in CI and gave false coverage confidence (the drift the audit flagged). - Re-homed 10 functional suites into the gate (renamed to *.unit.test.ts / *.integration.test.ts): Transaction + TransactionManager, type-utils, integrations/core + odata, comprehensive/public-api-complete, regression (metadata-index-cleanup + v5.7.0-deadlock), vfs/tree-operations + vfs-bulkwrite-race. ~192 previously-dark tests now run and pass. - Deleted 8 bit-rotted/redundant files that fail against 8.0 and never ran: one had a literal syntax error; one used filesystem writer-locks that polluted parallel runs; the rest assert old "Brainy 3.0/v3.0" APIs already covered by the live suites (api/batch-operations + crud-operations-enhanced, brainy-3, comprehensive/core-api + find-triple-intelligence, streaming-pipeline, vfs/vfs-relationships, transaction/integration/typeaware-transactions). - Added a coverage guard (tests/unit/test-suite-coverage-guard.test.ts) that FAILS CI if any *.test.ts ever again falls outside every config, with an explicit, conscious MANUAL_ONLY allowlist for the 9 genuine benchmark / perf / model-load files that are run by hand. Gate green: unit 1712, integration 607. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
396 lines
10 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
})
|