mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
b3d2018ee2
Critical bug fixes and code quality improvements from brutal-honest-review. ## Critical Fixes (9/10 completed, 1 deferred) 1. Fixed Agent Pattern Isolation (CRITICAL) - Added SwarmMemoryManager.queryPatternsByAgent() - SQL filtering by agent_id in metadata - Each agent now isolated 2. Fixed Async Method Cascade - Changed getPatterns() to async - Fixed 10 callers across 6 files - Build passes 3. Fixed Misleading Logging - Added isRealAgentDB() detection - Honest mock vs real reporting 4. Consolidated Embedding Generation - Created EmbeddingGenerator utility - Removed 50+ duplicate lines 5. Added AgentDB Initialization Checks - Empty DB detection - HNSW index verification 6. Integration Test Suite - 26 new test cases - Error path coverage 7. Repository Cleanup - Removed 19 temp test files 8. New QE Skill: sherlock-review - Evidence-based investigative review - 38 QE skills total 9. Deferred: Wire Up Real Test Execution - Deferred to v1.9.0 (documented) ## Statistics - Fixes: 9/10 (90%, 1 deferred) - Files Modified: 16 - Files Created: 3 - Files Deleted: 19 - Integration Tests: 26 - Build: PASSING ## Breaking Changes - getPatterns() now async - add await 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
464 lines
16 KiB
TypeScript
464 lines
16 KiB
TypeScript
/**
|
|
* Rollback Tests
|
|
* Tests database rollback functionality, state restoration, and system recovery
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import Database from 'better-sqlite3';
|
|
import * as crypto from 'crypto';
|
|
|
|
/**
|
|
* Calculate checksum for verification
|
|
*/
|
|
function calculateChecksum(dbPath: string): string {
|
|
const fileBuffer = fs.readFileSync(dbPath);
|
|
const hashSum = crypto.createHash('sha256');
|
|
hashSum.update(fileBuffer);
|
|
return hashSum.digest('hex');
|
|
}
|
|
|
|
describe('Rollback Functionality - Integration Tests', () => {
|
|
let testDir: string;
|
|
let originalDbPath: string;
|
|
let migratedDbPath: string;
|
|
let backupDir: string;
|
|
|
|
beforeEach(() => {
|
|
testDir = path.join(__dirname, '../.tmp/rollback-test-' + Date.now());
|
|
fs.mkdirSync(testDir, { recursive: true });
|
|
|
|
originalDbPath = path.join(testDir, 'original.db');
|
|
migratedDbPath = path.join(testDir, 'migrated.db');
|
|
backupDir = path.join(testDir, 'backups');
|
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
|
|
// Create original database (v1 schema)
|
|
const originalDb = new Database(originalDbPath);
|
|
originalDb.exec(`
|
|
CREATE TABLE schema_version (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at INTEGER NOT NULL,
|
|
description TEXT
|
|
);
|
|
|
|
CREATE TABLE episodes (
|
|
id TEXT PRIMARY KEY,
|
|
task TEXT NOT NULL,
|
|
reward REAL,
|
|
created_at INTEGER
|
|
);
|
|
|
|
INSERT INTO schema_version VALUES (1, ${Date.now()}, 'v1 schema');
|
|
|
|
INSERT INTO episodes VALUES
|
|
('ep1', 'test-gen', 0.95, ${Date.now()}),
|
|
('ep2', 'coverage', 0.85, ${Date.now()}),
|
|
('ep3', 'security', 0.90, ${Date.now()});
|
|
`);
|
|
originalDb.close();
|
|
|
|
// Create backup of original
|
|
const backupPath = path.join(backupDir, `original.db.backup.${Date.now()}.db`);
|
|
fs.copyFileSync(originalDbPath, backupPath);
|
|
const checksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(`${backupPath}.sha256`, checksum);
|
|
|
|
// Create migrated database (v2 schema)
|
|
fs.copyFileSync(originalDbPath, migratedDbPath);
|
|
const migratedDb = new Database(migratedDbPath);
|
|
migratedDb.exec(`
|
|
ALTER TABLE episodes ADD COLUMN success INTEGER DEFAULT 1;
|
|
ALTER TABLE episodes ADD COLUMN tokens_used INTEGER DEFAULT 0;
|
|
|
|
CREATE TABLE patterns (
|
|
id TEXT PRIMARY KEY,
|
|
pattern_type TEXT NOT NULL,
|
|
agent_id TEXT,
|
|
success_rate REAL DEFAULT 1.0
|
|
);
|
|
|
|
INSERT INTO schema_version VALUES (2, ${Date.now()}, 'v2 schema - add patterns');
|
|
`);
|
|
migratedDb.close();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('Rollback to Original State', () => {
|
|
it('should restore original schema', () => {
|
|
// Get backup
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
expect(backups.length).toBeGreaterThan(0);
|
|
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
|
|
// Verify migrated has v2 schema
|
|
let migratedDb = new Database(migratedDbPath, { readonly: true });
|
|
const v2Columns = migratedDb.prepare('PRAGMA table_info(episodes)').all() as Array<{ name: string }>;
|
|
migratedDb.close();
|
|
|
|
expect(v2Columns.map(c => c.name)).toContain('success');
|
|
expect(v2Columns.map(c => c.name)).toContain('tokens_used');
|
|
|
|
// Rollback: restore from backup
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify v1 schema restored
|
|
migratedDb = new Database(migratedDbPath, { readonly: true });
|
|
const v1Columns = migratedDb.prepare('PRAGMA table_info(episodes)').all() as Array<{ name: string }>;
|
|
const columnNames = v1Columns.map(c => c.name);
|
|
migratedDb.close();
|
|
|
|
expect(columnNames).not.toContain('success');
|
|
expect(columnNames).not.toContain('tokens_used');
|
|
expect(columnNames).toContain('id');
|
|
expect(columnNames).toContain('task');
|
|
expect(columnNames).toContain('reward');
|
|
});
|
|
|
|
it('should restore original data', () => {
|
|
// Get original data
|
|
const originalDb = new Database(originalDbPath, { readonly: true });
|
|
const originalData = originalDb.prepare('SELECT * FROM episodes ORDER BY id').all();
|
|
originalDb.close();
|
|
|
|
// Rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify data restored
|
|
const restoredDb = new Database(migratedDbPath, { readonly: true });
|
|
const restoredData = restoredDb.prepare('SELECT id, task, reward, created_at FROM episodes ORDER BY id').all();
|
|
restoredDb.close();
|
|
|
|
expect(restoredData).toEqual(originalData);
|
|
});
|
|
|
|
it('should restore original table count', () => {
|
|
// Original has 2 tables (schema_version, episodes)
|
|
const originalDb = new Database(originalDbPath, { readonly: true });
|
|
const originalTables = originalDb.prepare(`
|
|
SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
`).get() as { count: number };
|
|
originalDb.close();
|
|
|
|
// Migrated has 3 tables (added patterns)
|
|
const migratedDb = new Database(migratedDbPath, { readonly: true });
|
|
const migratedTables = migratedDb.prepare(`
|
|
SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
`).get() as { count: number };
|
|
migratedDb.close();
|
|
|
|
expect(migratedTables.count).toBe(originalTables.count + 1);
|
|
|
|
// Rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify table count restored
|
|
const restoredDb = new Database(migratedDbPath, { readonly: true });
|
|
const restoredTables = restoredDb.prepare(`
|
|
SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
`).get() as { count: number };
|
|
restoredDb.close();
|
|
|
|
expect(restoredTables.count).toBe(originalTables.count);
|
|
});
|
|
});
|
|
|
|
describe('No Data Loss During Rollback', () => {
|
|
it('should preserve all episode records', () => {
|
|
const originalDb = new Database(originalDbPath, { readonly: true });
|
|
const originalCount = originalDb.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
originalDb.close();
|
|
|
|
// Rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify count
|
|
const restoredDb = new Database(migratedDbPath, { readonly: true });
|
|
const restoredCount = restoredDb.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
restoredDb.close();
|
|
|
|
expect(restoredCount.count).toBe(originalCount.count);
|
|
expect(restoredCount.count).toBe(3);
|
|
});
|
|
|
|
it('should have matching checksums after rollback', () => {
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
// Get stored checksum
|
|
const storedChecksum = fs.readFileSync(checksumPath, 'utf-8');
|
|
|
|
// Rollback
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify checksum matches
|
|
const restoredChecksum = calculateChecksum(migratedDbPath);
|
|
expect(restoredChecksum).toBe(storedChecksum);
|
|
});
|
|
|
|
it('should preserve exact data values', () => {
|
|
const originalDb = new Database(originalDbPath, { readonly: true });
|
|
const originalEp1 = originalDb.prepare('SELECT * FROM episodes WHERE id = ?').get('ep1');
|
|
originalDb.close();
|
|
|
|
// Rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify exact match
|
|
const restoredDb = new Database(migratedDbPath, { readonly: true });
|
|
const restoredEp1 = restoredDb.prepare('SELECT * FROM episodes WHERE id = ?').get('ep1');
|
|
restoredDb.close();
|
|
|
|
expect(restoredEp1).toEqual(originalEp1);
|
|
});
|
|
});
|
|
|
|
describe('System Functional After Rollback', () => {
|
|
beforeEach(() => {
|
|
// Perform rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
});
|
|
|
|
it('should allow querying data', () => {
|
|
const db = new Database(migratedDbPath, { readonly: true });
|
|
|
|
const results = db.prepare('SELECT * FROM episodes WHERE task = ?').all('test-gen');
|
|
|
|
db.close();
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect((results[0] as any).id).toBe('ep1');
|
|
});
|
|
|
|
it('should allow inserting new data', () => {
|
|
const db = new Database(migratedDbPath);
|
|
|
|
db.prepare('INSERT INTO episodes VALUES (?, ?, ?, ?)').run(
|
|
'ep4',
|
|
'new-task',
|
|
0.75,
|
|
Date.now()
|
|
);
|
|
|
|
const result = db.prepare('SELECT * FROM episodes WHERE id = ?').get('ep4');
|
|
const count = db.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
|
|
db.close();
|
|
|
|
expect(result).toBeDefined();
|
|
expect((result as any).task).toBe('new-task');
|
|
expect(count.count).toBe(4);
|
|
});
|
|
|
|
it('should allow updating existing data', () => {
|
|
const db = new Database(migratedDbPath);
|
|
|
|
db.prepare('UPDATE episodes SET reward = ? WHERE id = ?').run(0.99, 'ep1');
|
|
|
|
const result = db.prepare('SELECT reward FROM episodes WHERE id = ?').get('ep1') as { reward: number };
|
|
|
|
db.close();
|
|
|
|
expect(result.reward).toBe(0.99);
|
|
});
|
|
|
|
it('should allow deleting data', () => {
|
|
const db = new Database(migratedDbPath);
|
|
|
|
db.prepare('DELETE FROM episodes WHERE id = ?').run('ep3');
|
|
|
|
const result = db.prepare('SELECT * FROM episodes WHERE id = ?').get('ep3');
|
|
const count = db.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
|
|
db.close();
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(count.count).toBe(2);
|
|
});
|
|
|
|
it('should maintain database integrity', () => {
|
|
const db = new Database(migratedDbPath, { readonly: true });
|
|
|
|
// Run integrity check
|
|
const integrity = db.prepare('PRAGMA integrity_check').get() as { integrity_check: string };
|
|
|
|
db.close();
|
|
|
|
expect(integrity.integrity_check).toBe('ok');
|
|
});
|
|
|
|
it('should support transactions', () => {
|
|
const db = new Database(migratedDbPath);
|
|
|
|
db.exec('BEGIN TRANSACTION');
|
|
|
|
db.prepare('INSERT INTO episodes VALUES (?, ?, ?, ?)').run('ep5', 'tx-test', 0.5, Date.now());
|
|
db.prepare('INSERT INTO episodes VALUES (?, ?, ?, ?)').run('ep6', 'tx-test-2', 0.6, Date.now());
|
|
|
|
db.exec('COMMIT');
|
|
|
|
const count = db.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
|
|
db.close();
|
|
|
|
expect(count.count).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('Rollback Verification', () => {
|
|
it('should verify schema version after rollback', () => {
|
|
// Rollback
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Check version
|
|
const db = new Database(migratedDbPath, { readonly: true });
|
|
const version = db.prepare(`
|
|
SELECT version FROM schema_version ORDER BY version DESC LIMIT 1
|
|
`).get() as { version: number };
|
|
db.close();
|
|
|
|
expect(version.version).toBe(1);
|
|
});
|
|
|
|
it('should verify backup integrity before rollback', () => {
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
// Verify checksum
|
|
const storedChecksum = fs.readFileSync(checksumPath, 'utf-8');
|
|
const actualChecksum = calculateChecksum(backupPath);
|
|
|
|
expect(actualChecksum).toBe(storedChecksum);
|
|
});
|
|
|
|
it('should detect if rollback is necessary', () => {
|
|
// Check if migration succeeded
|
|
const db = new Database(migratedDbPath, { readonly: true });
|
|
|
|
try {
|
|
// Try to query new table
|
|
db.prepare('SELECT * FROM patterns LIMIT 1').get();
|
|
db.close();
|
|
|
|
// Migration exists - rollback might be needed if issues found
|
|
const needsRollback = false; // Would be true if validation failed
|
|
expect(needsRollback).toBe(false);
|
|
} catch (error) {
|
|
db.close();
|
|
// Migration failed - rollback needed
|
|
expect(error).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Partial Migration Rollback', () => {
|
|
it('should rollback failed partial migration', () => {
|
|
// Simulate partial migration failure
|
|
const partialDb = new Database(migratedDbPath);
|
|
|
|
try {
|
|
partialDb.exec('BEGIN TRANSACTION');
|
|
|
|
// Successful step
|
|
partialDb.exec('ALTER TABLE episodes ADD COLUMN new_field TEXT');
|
|
|
|
// Simulate failure (e.g., constraint violation)
|
|
// Force rollback
|
|
partialDb.exec('ROLLBACK');
|
|
|
|
partialDb.close();
|
|
|
|
// Verify new_field not added
|
|
const verifyDb = new Database(migratedDbPath, { readonly: true });
|
|
const columns = verifyDb.prepare('PRAGMA table_info(episodes)').all() as Array<{ name: string }>;
|
|
verifyDb.close();
|
|
|
|
expect(columns.map(c => c.name)).not.toContain('new_field');
|
|
} catch (error) {
|
|
partialDb.close();
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
it('should restore from backup after corruption', () => {
|
|
// Corrupt migrated database
|
|
fs.appendFileSync(migratedDbPath, 'CORRUPTED DATA');
|
|
|
|
// Verify corruption
|
|
expect(() => {
|
|
const db = new Database(migratedDbPath);
|
|
db.prepare('SELECT * FROM episodes').all();
|
|
db.close();
|
|
}).toThrow();
|
|
|
|
// Restore from backup
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, migratedDbPath);
|
|
|
|
// Verify restored
|
|
const db = new Database(migratedDbPath, { readonly: true });
|
|
const results = db.prepare('SELECT * FROM episodes').all();
|
|
db.close();
|
|
|
|
expect(results.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('Multi-Step Rollback', () => {
|
|
it('should support rollback through multiple versions', () => {
|
|
// Create v3
|
|
const v3DbPath = path.join(testDir, 'v3.db');
|
|
fs.copyFileSync(migratedDbPath, v3DbPath);
|
|
|
|
const v3Db = new Database(v3DbPath);
|
|
v3Db.exec(`
|
|
ALTER TABLE patterns ADD COLUMN metadata TEXT;
|
|
INSERT INTO schema_version VALUES (3, ${Date.now()}, 'v3 schema');
|
|
`);
|
|
v3Db.close();
|
|
|
|
// Rollback to v1
|
|
const backups = fs.readdirSync(backupDir).filter(f => f.endsWith('.db'));
|
|
const backupPath = path.join(backupDir, backups[0]);
|
|
fs.copyFileSync(backupPath, v3DbPath);
|
|
|
|
// Verify v1 schema
|
|
const rolledBackDb = new Database(v3DbPath, { readonly: true });
|
|
const version = rolledBackDb.prepare(`
|
|
SELECT version FROM schema_version ORDER BY version DESC LIMIT 1
|
|
`).get() as { version: number };
|
|
|
|
const tables = rolledBackDb.prepare(`
|
|
SELECT name FROM sqlite_master WHERE type='table' AND name='patterns'
|
|
`).all();
|
|
|
|
rolledBackDb.close();
|
|
|
|
expect(version.version).toBe(1);
|
|
expect(tables.length).toBe(0); // patterns table shouldn't exist in v1
|
|
});
|
|
});
|
|
});
|