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>
333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
/**
|
|
* Integration Tests for Backup and Restore System
|
|
* Tests end-to-end backup creation, verification, and restoration
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
|
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import Database from 'better-sqlite3';
|
|
import * as crypto from 'crypto';
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
/**
|
|
* Calculate SHA-256 checksum
|
|
*/
|
|
function calculateChecksum(filePath: string): string {
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
const hashSum = crypto.createHash('sha256');
|
|
hashSum.update(fileBuffer);
|
|
return hashSum.digest('hex');
|
|
}
|
|
|
|
describe('Backup and Restore System - Integration Tests', () => {
|
|
const testDir = path.join(__dirname, '../.tmp/backup-restore-test-' + Date.now());
|
|
const dbPath = path.join(testDir, 'agentdb.db');
|
|
const backupDir = path.join(testDir, 'backups');
|
|
|
|
beforeAll(() => {
|
|
// Create test environment
|
|
fs.mkdirSync(testDir, { recursive: true });
|
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
|
|
// Create test database
|
|
const db = new Database(dbPath);
|
|
db.exec(`
|
|
CREATE TABLE episodes (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
task TEXT NOT NULL,
|
|
reward REAL,
|
|
success INTEGER,
|
|
created_at INTEGER
|
|
);
|
|
|
|
CREATE TABLE patterns (
|
|
id TEXT PRIMARY KEY,
|
|
pattern_type TEXT NOT NULL,
|
|
agent_id TEXT,
|
|
domain TEXT DEFAULT 'general',
|
|
success_rate REAL DEFAULT 1.0
|
|
);
|
|
|
|
-- Insert test data
|
|
INSERT INTO episodes VALUES
|
|
('ep1', 'test-session', 'test-gen', 0.95, 1, ${Date.now()}),
|
|
('ep2', 'test-session', 'coverage', 0.85, 1, ${Date.now()}),
|
|
('ep3', 'test-session', 'security', 0.90, 1, ${Date.now()});
|
|
|
|
INSERT INTO patterns VALUES
|
|
('pat1', 'tdd-pattern', 'qe-test-gen', 'testing', 0.92),
|
|
('pat2', 'boundary-pattern', 'qe-coverage', 'analysis', 0.88);
|
|
`);
|
|
db.close();
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Cleanup test directory
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('Backup Creation', () => {
|
|
it('should create backup with timestamp and checksum', () => {
|
|
const timestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
// Create backup
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
const checksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(checksumPath, checksum);
|
|
|
|
// Verify backup exists
|
|
expect(fs.existsSync(backupPath)).toBe(true);
|
|
expect(fs.existsSync(checksumPath)).toBe(true);
|
|
|
|
// Verify checksums match
|
|
const originalChecksum = calculateChecksum(dbPath);
|
|
const backupChecksum = fs.readFileSync(checksumPath, 'utf-8');
|
|
|
|
expect(backupChecksum).toBe(calculateChecksum(backupPath));
|
|
});
|
|
|
|
it('should include all tables in backup', () => {
|
|
const timestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
|
|
const db = new Database(backupPath, { readonly: true });
|
|
const tables = db.prepare(`
|
|
SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
`).all() as Array<{ name: string }>;
|
|
db.close();
|
|
|
|
const tableNames = tables.map(t => t.name);
|
|
expect(tableNames).toContain('episodes');
|
|
expect(tableNames).toContain('patterns');
|
|
});
|
|
|
|
it('should preserve all data in backup', () => {
|
|
const timestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
|
|
// Get original counts
|
|
const originalDb = new Database(dbPath, { readonly: true });
|
|
const originalEpisodes = originalDb.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
const originalPatterns = originalDb.prepare('SELECT COUNT(*) as count FROM patterns').get() as { count: number };
|
|
originalDb.close();
|
|
|
|
// Create backup
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
|
|
// Verify backup counts
|
|
const backupDb = new Database(backupPath, { readonly: true });
|
|
const backupEpisodes = backupDb.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
const backupPatterns = backupDb.prepare('SELECT COUNT(*) as count FROM patterns').get() as { count: number };
|
|
backupDb.close();
|
|
|
|
expect(backupEpisodes.count).toBe(originalEpisodes.count);
|
|
expect(backupPatterns.count).toBe(originalPatterns.count);
|
|
});
|
|
});
|
|
|
|
describe('Backup Verification', () => {
|
|
it('should detect corrupted backup files', () => {
|
|
const timestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
// Create backup
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
const originalChecksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(checksumPath, originalChecksum);
|
|
|
|
// Corrupt backup
|
|
fs.appendFileSync(backupPath, 'CORRUPTED DATA');
|
|
|
|
// Verify corruption detected
|
|
const corruptChecksum = calculateChecksum(backupPath);
|
|
expect(corruptChecksum).not.toBe(originalChecksum);
|
|
});
|
|
|
|
it('should validate checksum file format', () => {
|
|
const timestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
const checksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(checksumPath, checksum);
|
|
|
|
const storedChecksum = fs.readFileSync(checksumPath, 'utf-8');
|
|
|
|
// Verify it's a valid SHA-256 hex string
|
|
expect(storedChecksum).toMatch(/^[a-f0-9]{64}$/);
|
|
});
|
|
});
|
|
|
|
describe('Database Restoration', () => {
|
|
let originalData: any[];
|
|
let backupTimestamp: number;
|
|
|
|
beforeEach(() => {
|
|
// Save original data
|
|
const db = new Database(dbPath, { readonly: true });
|
|
originalData = db.prepare('SELECT * FROM episodes ORDER BY id').all();
|
|
db.close();
|
|
|
|
// Create backup
|
|
backupTimestamp = Date.now();
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${backupTimestamp}.db`);
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
const checksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(`${backupPath}.sha256`, checksum);
|
|
});
|
|
|
|
it('should restore database from backup', () => {
|
|
// Modify current database
|
|
const db = new Database(dbPath);
|
|
db.exec('DELETE FROM episodes WHERE id = "ep1"');
|
|
const afterDelete = db.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
db.close();
|
|
|
|
expect(afterDelete.count).toBe(2);
|
|
|
|
// Restore from backup
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${backupTimestamp}.db`);
|
|
fs.copyFileSync(backupPath, dbPath);
|
|
|
|
// Verify restoration
|
|
const restored = new Database(dbPath, { readonly: true });
|
|
const restoredCount = restored.prepare('SELECT COUNT(*) as count FROM episodes').get() as { count: number };
|
|
const restoredData = restored.prepare('SELECT * FROM episodes ORDER BY id').all();
|
|
restored.close();
|
|
|
|
expect(restoredCount.count).toBe(3);
|
|
expect(restoredData).toEqual(originalData);
|
|
});
|
|
|
|
it('should verify checksum before restoration', () => {
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${backupTimestamp}.db`);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
// Read and verify checksum
|
|
const storedChecksum = fs.readFileSync(checksumPath, 'utf-8');
|
|
const actualChecksum = calculateChecksum(backupPath);
|
|
|
|
expect(actualChecksum).toBe(storedChecksum);
|
|
});
|
|
|
|
it('should maintain database integrity after restoration', () => {
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${backupTimestamp}.db`);
|
|
|
|
// Restore
|
|
fs.copyFileSync(backupPath, dbPath);
|
|
|
|
// Verify integrity
|
|
const db = new Database(dbPath, { readonly: true });
|
|
|
|
// Check tables exist
|
|
const tables = db.prepare(`
|
|
SELECT name FROM sqlite_master WHERE type='table'
|
|
`).all() as Array<{ name: string }>;
|
|
|
|
// Verify can query data
|
|
const episodes = db.prepare('SELECT * FROM episodes').all();
|
|
const patterns = db.prepare('SELECT * FROM patterns').all();
|
|
|
|
db.close();
|
|
|
|
expect(tables.length).toBeGreaterThan(0);
|
|
expect(episodes.length).toBe(3);
|
|
expect(patterns.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Backup Listing', () => {
|
|
it('should list all backups with timestamps', () => {
|
|
// Create multiple backups
|
|
const timestamps = [Date.now(), Date.now() + 1000, Date.now() + 2000];
|
|
|
|
timestamps.forEach(timestamp => {
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
const checksum = calculateChecksum(backupPath);
|
|
fs.writeFileSync(`${backupPath}.sha256`, checksum);
|
|
});
|
|
|
|
// List backups
|
|
const backups = fs.readdirSync(backupDir)
|
|
.filter(f => f.includes('.backup.') && f.endsWith('.db'));
|
|
|
|
expect(backups.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('should identify latest backup', () => {
|
|
// Create backups with known timestamps
|
|
const t1 = 1000000000;
|
|
const t2 = 2000000000;
|
|
const t3 = 3000000000;
|
|
|
|
[t1, t2, t3].forEach(timestamp => {
|
|
const backupPath = path.join(backupDir, `agentdb.db.backup.${timestamp}.db`);
|
|
fs.copyFileSync(dbPath, backupPath);
|
|
});
|
|
|
|
// Find latest
|
|
const backups = fs.readdirSync(backupDir)
|
|
.filter(f => f.includes('.backup.') && f.endsWith('.db'))
|
|
.map(f => {
|
|
const match = f.match(/\.backup\.(\d+)\.db$/);
|
|
return match ? parseInt(match[1]) : 0;
|
|
})
|
|
.sort((a, b) => b - a);
|
|
|
|
expect(backups[0]).toBe(t3);
|
|
});
|
|
});
|
|
|
|
describe('Cleanup Operations', () => {
|
|
it('should remove old backups', () => {
|
|
// Create old backups
|
|
const oldTimestamp = Date.now() - (31 * 24 * 60 * 60 * 1000); // 31 days ago
|
|
const oldBackupPath = path.join(backupDir, `agentdb.db.backup.${oldTimestamp}.db`);
|
|
|
|
fs.copyFileSync(dbPath, oldBackupPath);
|
|
expect(fs.existsSync(oldBackupPath)).toBe(true);
|
|
|
|
// Simulate cleanup
|
|
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
const now = Date.now();
|
|
|
|
const backups = fs.readdirSync(backupDir)
|
|
.filter(f => f.includes('.backup.') && f.endsWith('.db'));
|
|
|
|
backups.forEach(backup => {
|
|
const match = backup.match(/\.backup\.(\d+)\.db$/);
|
|
if (match) {
|
|
const timestamp = parseInt(match[1]);
|
|
const age = now - timestamp;
|
|
|
|
if (age > maxAge) {
|
|
const backupPath = path.join(backupDir, backup);
|
|
const checksumPath = `${backupPath}.sha256`;
|
|
|
|
fs.unlinkSync(backupPath);
|
|
if (fs.existsSync(checksumPath)) {
|
|
fs.unlinkSync(checksumPath);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(fs.existsSync(oldBackupPath)).toBe(false);
|
|
});
|
|
});
|
|
});
|