mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
3b81b10d30
Brain export v3.0 (25 tables), witness chain v3 with Ed25519 signing, streaming for large tables, RVF freeze/derive/indexStats, npm packaging fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
766 lines
31 KiB
TypeScript
766 lines
31 KiB
TypeScript
/**
|
|
* Brain Export/Import System Tests
|
|
*
|
|
* Tests the portable brain export that snapshots learning state
|
|
* (patterns, Q-values, dream insights, witness chain) and imports
|
|
* it into another AQE instance.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import Database from 'better-sqlite3';
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { randomUUID } from 'crypto';
|
|
import {
|
|
exportBrain,
|
|
importBrain,
|
|
brainInfo,
|
|
computeChecksum,
|
|
type BrainExportManifest,
|
|
type BrainExportOptions,
|
|
type BrainImportOptions,
|
|
} from '../../src/integrations/ruvector/brain-exporter.js';
|
|
import { ensureTargetTables, TABLE_CONFIGS } from '../../src/integrations/ruvector/brain-shared.js';
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
function createTestDb(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.pragma('journal_mode = WAL');
|
|
ensureTargetTables(db);
|
|
return db;
|
|
}
|
|
|
|
function seedTestData(db: Database.Database): void {
|
|
// Patterns
|
|
db.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p1', 'test-template', 'test-generation', 'test-generation', 'AAA Test', 'Arrange-Act-Assert', 0.8, '2026-02-20T10:00:00Z');
|
|
|
|
db.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p2', 'mock-pattern', 'coverage-analysis', 'coverage-analysis', 'Coverage Strategy', 'Risk-based coverage', 0.6, '2026-02-20T11:00:00Z');
|
|
|
|
db.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p3', 'assertion-pattern', 'security-compliance', 'security-compliance', 'OWASP Check', 'OWASP Top 10', 0.9, '2026-02-20T12:00:00Z');
|
|
|
|
// Q-Values
|
|
db.prepare(`
|
|
INSERT INTO rl_q_values (id, algorithm, agent_id, state_key, action_key, q_value, visits, domain, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('qv1', 'sarsa', 'agent-1', 'state-a', 'action-x', 0.75, 10, 'test-generation', '2026-02-20T10:00:00Z');
|
|
|
|
db.prepare(`
|
|
INSERT INTO rl_q_values (id, algorithm, agent_id, state_key, action_key, q_value, visits, domain, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('qv2', 'q-learning', 'agent-2', 'state-b', 'action-y', 0.5, 5, 'coverage-analysis', '2026-02-20T11:00:00Z');
|
|
|
|
// Dream cycle (needed as FK parent for insights)
|
|
db.prepare(`
|
|
INSERT INTO dream_cycles (id, start_time, status)
|
|
VALUES (?, ?, ?)
|
|
`).run('dc1', '2026-02-20T09:00:00Z', 'completed');
|
|
|
|
// Dream Insights
|
|
db.prepare(`
|
|
INSERT INTO dream_insights (id, cycle_id, insight_type, source_concepts, description, confidence_score)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run('di1', 'dc1', 'cross-pattern', '["p1","p2"]', 'AAA and coverage are related', 0.7);
|
|
|
|
db.prepare(`
|
|
INSERT INTO dream_insights (id, cycle_id, insight_type, source_concepts, description, confidence_score)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run('di2', 'dc1', 'novel-association', '["p3"]', 'Security patterns overlap with quality', 0.4);
|
|
|
|
// Witness Chain
|
|
db.prepare(`
|
|
INSERT INTO witness_chain (prev_hash, action_hash, action_type, action_data, timestamp, actor)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run('0'.repeat(64), 'abc123', 'PATTERN_CREATE', '{"patternId":"p1"}', '2026-02-20T10:00:00Z', 'reasoning-bank');
|
|
|
|
db.prepare(`
|
|
INSERT INTO witness_chain (prev_hash, action_hash, action_type, action_data, timestamp, actor)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run('def456', 'ghi789', 'PATTERN_UPDATE', '{"patternId":"p1","success":true}', '2026-02-20T10:30:00Z', 'reasoning-bank');
|
|
|
|
// Vectors
|
|
const buf = Buffer.alloc(4 * 3); // 3 float32s
|
|
new Float32Array(buf.buffer).set([0.1, 0.2, 0.3]);
|
|
db.prepare(`
|
|
INSERT INTO vectors (id, namespace, embedding, dimensions) VALUES (?, ?, ?, ?)
|
|
`).run('v1', 'default', buf, 3);
|
|
}
|
|
|
|
let tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = join(tmpdir(), `brain-test-${randomUUID()}`);
|
|
mkdirSync(dir, { recursive: true });
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('Brain Export/Import System', () => {
|
|
let sourceDb: Database.Database;
|
|
|
|
beforeEach(() => {
|
|
sourceDb = createTestDb();
|
|
seedTestData(sourceDb);
|
|
tempDirs = [];
|
|
});
|
|
|
|
afterEach(() => {
|
|
sourceDb.close();
|
|
for (const dir of tempDirs) {
|
|
if (existsSync(dir)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Export tests
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('exportBrain', () => {
|
|
it('should export all data with correct manifest stats', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
includeQValues: true,
|
|
includeDreamInsights: true,
|
|
includeWitnessChain: true,
|
|
includeVectors: true,
|
|
});
|
|
|
|
expect(manifest.version).toBe('3.0');
|
|
expect(manifest.stats.patternCount).toBe(3);
|
|
expect(manifest.stats.qValueCount).toBe(2);
|
|
expect(manifest.stats.dreamInsightCount).toBe(2);
|
|
expect(manifest.stats.witnessChainLength).toBe(2);
|
|
expect(manifest.stats.vectorCount).toBe(1);
|
|
expect(manifest.domains).toContain('test-generation');
|
|
expect(manifest.domains).toContain('coverage-analysis');
|
|
expect(manifest.domains).toContain('security-compliance');
|
|
expect(manifest.checksum).toBeTruthy();
|
|
expect(manifest.checksum.length).toBe(64); // SHA-256 hex
|
|
|
|
// Verify files exist
|
|
expect(existsSync(join(outDir, 'manifest.json'))).toBe(true);
|
|
expect(existsSync(join(outDir, 'patterns.jsonl'))).toBe(true);
|
|
expect(existsSync(join(outDir, 'q-values.jsonl'))).toBe(true);
|
|
expect(existsSync(join(outDir, 'dream-insights.jsonl'))).toBe(true);
|
|
expect(existsSync(join(outDir, 'witness-chain.jsonl'))).toBe(true);
|
|
|
|
// Verify JSONL line counts
|
|
const patternLines = readFileSync(join(outDir, 'patterns.jsonl'), 'utf-8').trim().split('\n');
|
|
expect(patternLines.length).toBe(3);
|
|
|
|
const qLines = readFileSync(join(outDir, 'q-values.jsonl'), 'utf-8').trim().split('\n');
|
|
expect(qLines.length).toBe(2);
|
|
});
|
|
|
|
it('should filter patterns by domain', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
domains: ['test-generation'],
|
|
});
|
|
|
|
expect(manifest.stats.patternCount).toBe(1);
|
|
expect(manifest.domains).toEqual(['test-generation']);
|
|
|
|
const patternLines = readFileSync(join(outDir, 'patterns.jsonl'), 'utf-8').trim().split('\n');
|
|
expect(patternLines.length).toBe(1);
|
|
const parsed = JSON.parse(patternLines[0]);
|
|
expect(parsed.qe_domain).toBe('test-generation');
|
|
});
|
|
|
|
it('should exclude optional sections when disabled', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
includeQValues: false,
|
|
includeDreamInsights: false,
|
|
includeWitnessChain: false,
|
|
includeVectors: false,
|
|
});
|
|
|
|
expect(manifest.stats.patternCount).toBe(3);
|
|
expect(manifest.stats.qValueCount).toBe(0);
|
|
expect(manifest.stats.dreamInsightCount).toBe(0);
|
|
expect(manifest.stats.witnessChainLength).toBe(0);
|
|
expect(manifest.stats.vectorCount).toBe(0);
|
|
});
|
|
|
|
it('should create output directory if it does not exist', () => {
|
|
const outDir = join(makeTempDir(), 'nested', 'deep');
|
|
expect(existsSync(outDir)).toBe(false);
|
|
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
expect(existsSync(outDir)).toBe(true);
|
|
expect(manifest.stats.patternCount).toBe(3);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Import tests
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('importBrain', () => {
|
|
it('should import into empty database with all records', () => {
|
|
// Export
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Import into fresh DB
|
|
const targetDb = createTestDb();
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
// 3 patterns + 2 qvalues + 1 dream_cycle + 2 insights + 2 witness + 1 vector = 11
|
|
expect(result.imported).toBe(11);
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.conflicts).toBe(0);
|
|
|
|
// Verify target DB has the data
|
|
const patternCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_patterns').get() as { cnt: number }).cnt;
|
|
expect(patternCount).toBe(3);
|
|
|
|
const qvCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM rl_q_values').get() as { cnt: number }).cnt;
|
|
expect(qvCount).toBe(2);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should skip conflicts with skip-conflicts strategy', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Import into DB that already has the same data
|
|
const targetDb = createTestDb();
|
|
seedTestData(targetDb);
|
|
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
expect(result.imported).toBe(0);
|
|
expect(result.skipped).toBeGreaterThan(0);
|
|
expect(result.conflicts).toBeGreaterThan(0);
|
|
|
|
// Data should be unchanged
|
|
const patternCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_patterns').get() as { cnt: number }).cnt;
|
|
expect(patternCount).toBe(3);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should overwrite with higher confidence using highest-confidence strategy', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Create target with same patterns but LOWER confidence
|
|
const targetDb = createTestDb();
|
|
targetDb.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p1', 'test-template', 'test-generation', 'test-generation', 'Old Name', 'Old desc', 0.3, '2026-01-01T00:00:00Z');
|
|
|
|
// p1 in source has confidence 0.8, target has 0.3 => import wins
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'highest-confidence' });
|
|
|
|
// p1 imported (0.8 > 0.3), p2 and p3 are new (no conflict)
|
|
expect(result.imported).toBeGreaterThan(0);
|
|
|
|
const row = targetDb.prepare('SELECT confidence, name FROM qe_patterns WHERE id = ?').get('p1') as { confidence: number; name: string };
|
|
expect(row.confidence).toBe(0.8);
|
|
expect(row.name).toBe('AAA Test'); // Overwritten from source
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should not overwrite when existing confidence is higher', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
targetDb.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p1', 'test-template', 'test-generation', 'test-generation', 'Better Pattern', 'Higher confidence', 0.95, '2026-03-01T00:00:00Z');
|
|
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'highest-confidence' });
|
|
|
|
const row = targetDb.prepare('SELECT confidence, name FROM qe_patterns WHERE id = ?').get('p1') as { confidence: number; name: string };
|
|
expect(row.confidence).toBe(0.95); // Kept existing
|
|
expect(row.name).toBe('Better Pattern');
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should use latest-wins strategy based on updated_at timestamp', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
// p1 in source: updated_at = 2026-02-20T10:00:00Z
|
|
// p1 in target: updated_at = 2026-03-01 (newer) => target wins
|
|
targetDb.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name, description, confidence, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('p1', 'test-template', 'test-generation', 'test-generation', 'Newer Pattern', 'Latest', 0.5, '2026-03-01T00:00:00Z');
|
|
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'latest-wins' });
|
|
|
|
const row = targetDb.prepare('SELECT name FROM qe_patterns WHERE id = ?').get('p1') as { name: string };
|
|
expect(row.name).toBe('Newer Pattern'); // Target was newer, kept
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should handle dry-run without modifying the database', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
const result = importBrain(targetDb, outDir, {
|
|
mergeStrategy: 'skip-conflicts',
|
|
dryRun: true,
|
|
});
|
|
|
|
expect(result.imported).toBeGreaterThan(0);
|
|
|
|
// Database should still be empty (tables exist but no rows)
|
|
const patternCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_patterns').get() as { cnt: number }).cnt;
|
|
expect(patternCount).toBe(0);
|
|
|
|
targetDb.close();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Round-trip tests
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('round-trip export then import', () => {
|
|
it('should preserve all data through export and import into empty DB', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
// All records imported
|
|
expect(result.imported).toBe(manifest.stats.totalRecords ?? (
|
|
manifest.stats.patternCount + manifest.stats.qValueCount +
|
|
manifest.stats.dreamInsightCount + manifest.stats.witnessChainLength
|
|
));
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.conflicts).toBe(0);
|
|
|
|
// Verify counts match
|
|
const patternCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_patterns').get() as { cnt: number }).cnt;
|
|
expect(patternCount).toBe(manifest.stats.patternCount);
|
|
|
|
const qvCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM rl_q_values').get() as { cnt: number }).cnt;
|
|
expect(qvCount).toBe(manifest.stats.qValueCount);
|
|
|
|
const insightCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM dream_insights').get() as { cnt: number }).cnt;
|
|
expect(insightCount).toBe(manifest.stats.dreamInsightCount);
|
|
|
|
const witnessCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM witness_chain').get() as { cnt: number }).cnt;
|
|
expect(witnessCount).toBe(manifest.stats.witnessChainLength);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should preserve pattern field values through round-trip', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const original = sourceDb.prepare('SELECT * FROM qe_patterns WHERE id = ?').get('p1') as Record<string, unknown>;
|
|
const imported = targetDb.prepare('SELECT * FROM qe_patterns WHERE id = ?').get('p1') as Record<string, unknown>;
|
|
|
|
expect(imported.name).toBe(original.name);
|
|
expect(imported.description).toBe(original.description);
|
|
expect(imported.confidence).toBe(original.confidence);
|
|
expect(imported.pattern_type).toBe(original.pattern_type);
|
|
expect(imported.qe_domain).toBe(original.qe_domain);
|
|
|
|
targetDb.close();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// brainInfo tests
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('brainInfo', () => {
|
|
it('should read manifest correctly', () => {
|
|
const outDir = makeTempDir();
|
|
const exported = exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const info = brainInfo(outDir);
|
|
|
|
expect(info.version).toBe('3.0');
|
|
expect(info.stats.patternCount).toBe(exported.stats.patternCount);
|
|
expect(info.stats.qValueCount).toBe(exported.stats.qValueCount);
|
|
expect(info.checksum).toBe(exported.checksum);
|
|
expect(info.domains).toEqual(exported.domains);
|
|
});
|
|
|
|
it('should throw when manifest does not exist', () => {
|
|
const emptyDir = makeTempDir();
|
|
expect(() => brainInfo(emptyDir)).toThrow('Manifest not found');
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Checksum validation
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('checksum validation', () => {
|
|
it('should reject import when a file has been tampered with', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Tamper with patterns.jsonl
|
|
const patternsPath = join(outDir, 'patterns.jsonl');
|
|
const content = readFileSync(patternsPath, 'utf-8');
|
|
writeFileSync(patternsPath, content + '{"id":"evil","pattern_type":"x","qe_domain":"y","domain":"z","name":"hacked"}\n');
|
|
|
|
const targetDb = createTestDb();
|
|
expect(() => {
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
}).toThrow(/[Cc]hecksum mismatch/);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should reject import when manifest checksum is wrong', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Tamper with the manifest checksum directly
|
|
const manifestPath = join(outDir, 'manifest.json');
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
manifest.checksum = 'deadbeef'.repeat(8);
|
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
|
|
const targetDb = createTestDb();
|
|
expect(() => {
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
}).toThrow(/[Cc]hecksum mismatch/);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should accept import when files are untampered', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
const targetDb = createTestDb();
|
|
// Should not throw
|
|
const result = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
expect(result.imported).toBeGreaterThan(0);
|
|
|
|
targetDb.close();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Edge cases
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty database gracefully', () => {
|
|
const emptyDb = createTestDb();
|
|
const outDir = makeTempDir();
|
|
|
|
const manifest = exportBrain(emptyDb, { outputPath: outDir });
|
|
|
|
expect(manifest.stats.patternCount).toBe(0);
|
|
expect(manifest.stats.qValueCount).toBe(0);
|
|
expect(manifest.stats.dreamInsightCount).toBe(0);
|
|
expect(manifest.stats.witnessChainLength).toBe(0);
|
|
expect(manifest.domains).toEqual([]);
|
|
|
|
emptyDb.close();
|
|
});
|
|
|
|
it('should handle database without optional tables', () => {
|
|
const bareDb = new Database(':memory:');
|
|
// Only create qe_patterns, skip others
|
|
bareDb.exec(`
|
|
CREATE TABLE qe_patterns (
|
|
id TEXT PRIMARY KEY,
|
|
pattern_type TEXT NOT NULL,
|
|
qe_domain TEXT NOT NULL,
|
|
domain TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
confidence REAL DEFAULT 0.5,
|
|
usage_count INTEGER DEFAULT 0,
|
|
success_rate REAL DEFAULT 0.0,
|
|
quality_score REAL DEFAULT 0.0,
|
|
tier TEXT DEFAULT 'short-term',
|
|
template_json TEXT,
|
|
context_json TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
last_used_at TEXT,
|
|
successful_uses INTEGER DEFAULT 0,
|
|
tokens_used INTEGER,
|
|
input_tokens INTEGER,
|
|
output_tokens INTEGER,
|
|
latency_ms REAL,
|
|
reusable INTEGER DEFAULT 0,
|
|
reuse_count INTEGER DEFAULT 0,
|
|
average_token_savings REAL DEFAULT 0,
|
|
total_tokens_saved INTEGER
|
|
);
|
|
`);
|
|
bareDb.prepare(`
|
|
INSERT INTO qe_patterns (id, pattern_type, qe_domain, domain, name) VALUES (?, ?, ?, ?, ?)
|
|
`).run('p1', 'test-template', 'test-generation', 'test-generation', 'Test');
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(bareDb, { outputPath: outDir });
|
|
|
|
expect(manifest.stats.patternCount).toBe(1);
|
|
expect(manifest.stats.qValueCount).toBe(0);
|
|
expect(manifest.stats.dreamInsightCount).toBe(0);
|
|
expect(manifest.stats.witnessChainLength).toBe(0);
|
|
|
|
bareDb.close();
|
|
});
|
|
|
|
it('should handle domain filter with multiple domains', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
domains: ['test-generation', 'security-compliance'],
|
|
});
|
|
|
|
expect(manifest.stats.patternCount).toBe(2); // p1 and p3
|
|
expect(manifest.domains).toContain('test-generation');
|
|
expect(manifest.domains).toContain('security-compliance');
|
|
expect(manifest.domains).not.toContain('coverage-analysis');
|
|
});
|
|
|
|
it('should handle domain filter that matches no patterns', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
domains: ['nonexistent-domain'],
|
|
});
|
|
|
|
expect(manifest.stats.patternCount).toBe(0);
|
|
expect(manifest.domains).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Phase 2: New table export/import tests
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('Phase 2 tables', () => {
|
|
it('should export and import captured_experiences', () => {
|
|
sourceDb.prepare(`
|
|
INSERT INTO captured_experiences (id, task, agent, domain, success, quality, duration_ms, completed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('exp-1', 'test-task', 'agent-1', 'test-generation', 1, 0.85, 500, '2026-02-20T10:00:00Z');
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
expect(manifest.tableRecordCounts?.captured_experiences).toBe(1);
|
|
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const row = targetDb.prepare('SELECT * FROM captured_experiences WHERE id = ?').get('exp-1') as Record<string, unknown>;
|
|
expect(row.task).toBe('test-task');
|
|
expect(row.quality).toBe(0.85);
|
|
expect(row.domain).toBe('test-generation');
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should export and import sona_patterns', () => {
|
|
sourceDb.prepare(`
|
|
INSERT INTO sona_patterns (id, type, domain, action_type, outcome_reward, outcome_success, outcome_quality, confidence)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('sona-1', 'test', 'test-generation', 'generate', 0.9, 1, 0.8, 0.75);
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
expect(manifest.tableRecordCounts?.sona_patterns).toBe(1);
|
|
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const row = targetDb.prepare('SELECT * FROM sona_patterns WHERE id = ?').get('sona-1') as Record<string, unknown>;
|
|
expect(row.type).toBe('test');
|
|
expect(row.confidence).toBe(0.75);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should export and import goap_actions with domain filter', () => {
|
|
sourceDb.prepare(`
|
|
INSERT INTO goap_actions (id, name, agent_type, preconditions, effects, category, qe_domain)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run('ga-1', 'RunTests', 'tester', '{}', '{}', 'testing', 'test-generation');
|
|
sourceDb.prepare(`
|
|
INSERT INTO goap_actions (id, name, agent_type, preconditions, effects, category, qe_domain)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run('ga-2', 'ScanSecurity', 'auditor', '{}', '{}', 'security', 'security-compliance');
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, {
|
|
outputPath: outDir,
|
|
domains: ['test-generation'],
|
|
});
|
|
|
|
// Only ga-1 should be exported (domain filtered)
|
|
expect(manifest.tableRecordCounts?.goap_actions).toBe(1);
|
|
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const count = (targetDb.prepare('SELECT COUNT(*) as cnt FROM goap_actions').get() as { cnt: number }).cnt;
|
|
expect(count).toBe(1);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should export and import concept_nodes and concept_edges', () => {
|
|
sourceDb.prepare(`
|
|
INSERT INTO concept_nodes (id, concept_type, content, activation_level)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run('cn-1', 'pattern', 'Test pattern concept', 0.8);
|
|
sourceDb.prepare(`
|
|
INSERT INTO concept_nodes (id, concept_type, content, activation_level)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run('cn-2', 'domain', 'Coverage domain', 0.6);
|
|
sourceDb.prepare(`
|
|
INSERT INTO concept_edges (id, source, target, weight, edge_type, evidence)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run('ce-1', 'cn-1', 'cn-2', 0.9, 'related', 5);
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
expect(manifest.tableRecordCounts?.concept_nodes).toBe(2);
|
|
expect(manifest.tableRecordCounts?.concept_edges).toBe(1);
|
|
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const nodeCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM concept_nodes').get() as { cnt: number }).cnt;
|
|
expect(nodeCount).toBe(2);
|
|
|
|
const edgeCount = (targetDb.prepare('SELECT COUNT(*) as cnt FROM concept_edges').get() as { cnt: number }).cnt;
|
|
expect(edgeCount).toBe(1);
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should export and import qe_pattern_usage with dedup', () => {
|
|
sourceDb.prepare(`
|
|
INSERT INTO qe_pattern_usage (pattern_id, success, metrics_json, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run('p1', 1, '{"tokens":100}', '2026-02-20T10:00:00Z');
|
|
sourceDb.prepare(`
|
|
INSERT INTO qe_pattern_usage (pattern_id, success, metrics_json, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run('p1', 0, '{"tokens":200}', '2026-02-20T11:00:00Z');
|
|
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
expect(manifest.tableRecordCounts?.qe_pattern_usage).toBe(2);
|
|
|
|
// First import
|
|
const targetDb = createTestDb();
|
|
const result1 = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
const usageCount1 = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_pattern_usage').get() as { cnt: number }).cnt;
|
|
expect(usageCount1).toBe(2);
|
|
|
|
// Second import should dedup
|
|
const result2 = importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
const usageCount2 = (targetDb.prepare('SELECT COUNT(*) as cnt FROM qe_pattern_usage').get() as { cnt: number }).cnt;
|
|
expect(usageCount2).toBe(2); // No duplicates added
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should round-trip captured_experiences with BLOB embedding', () => {
|
|
// Create a float32 embedding
|
|
const dims = 8;
|
|
const float32 = new Float32Array(dims);
|
|
for (let i = 0; i < dims; i++) float32[i] = (i + 1) * 0.1;
|
|
const embeddingBuf = Buffer.from(float32.buffer);
|
|
|
|
sourceDb.prepare(`
|
|
INSERT INTO captured_experiences (id, task, agent, domain, success, quality, duration_ms, completed_at, embedding, embedding_dimension)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run('exp-blob', 'task', 'agent', 'test', 1, 0.9, 100, '2026-02-20T10:00:00Z', embeddingBuf, dims);
|
|
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
// Verify JSONL contains Base64 encoded blob
|
|
const jsonlContent = readFileSync(join(outDir, 'captured-experiences.jsonl'), 'utf-8');
|
|
const parsed = JSON.parse(jsonlContent.trim());
|
|
expect(parsed._embedding_b64).toBeDefined();
|
|
expect(parsed.embedding).toBeUndefined();
|
|
|
|
// Import and verify round-trip
|
|
const targetDb = createTestDb();
|
|
importBrain(targetDb, outDir, { mergeStrategy: 'skip-conflicts' });
|
|
|
|
const row = targetDb.prepare('SELECT embedding, embedding_dimension FROM captured_experiences WHERE id = ?').get('exp-blob') as { embedding: Buffer; embedding_dimension: number };
|
|
expect(row.embedding).toBeInstanceOf(Buffer);
|
|
expect(row.embedding_dimension).toBe(dims);
|
|
|
|
const restored = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, dims);
|
|
for (let i = 0; i < dims; i++) {
|
|
expect(restored[i]).toBeCloseTo(float32[i], 5);
|
|
}
|
|
|
|
targetDb.close();
|
|
});
|
|
|
|
it('should export all 25 JSONL files', () => {
|
|
const outDir = makeTempDir();
|
|
exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
for (const config of TABLE_CONFIGS) {
|
|
expect(existsSync(join(outDir, config.fileName))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should include totalRecords and tableRecordCounts in manifest', () => {
|
|
const outDir = makeTempDir();
|
|
const manifest = exportBrain(sourceDb, { outputPath: outDir });
|
|
|
|
expect(manifest.stats.totalRecords).toBeGreaterThan(0);
|
|
expect(manifest.tableRecordCounts).toBeDefined();
|
|
expect(manifest.tableRecordCounts!['qe_patterns']).toBe(3);
|
|
});
|
|
});
|
|
});
|