/** * Smoke test: ADR-130 Phase 1 — graph_edges schema migration * * Acceptance criteria (ADR-130 §Phase 1): * 1. graph_edges table created by ruvector setup (MEMORY_SCHEMA_V3) without error * 2. agentdb_causal-edge inserts a row with 384-dim embedding blob * 3. Legacy unprefixed ID is auto-prefixed as "mem:" with deprecation warning * 4. Double-write to graph-node native retained (tested via isGraphBackendAvailable guard) * * Runs without @ruvector/graph-node — tests the sql.js fallback path. * * Usage: node scripts/smoke-graph-schema-migration.mjs */ import { fileURLToPath } from 'url'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '..'); // sql.js is available in root node_modules (installed by the CI setup step via // `npm install --legacy-peer-deps --ignore-scripts` at the repo root). // This matches the pattern used by smoke-memory-stats-legacy-db.mjs which passes CI. async function loadSqlJs() { const mod = await import('sql.js'); const initSqlJs = mod.default ?? mod; return await initSqlJs(); } // ─── helpers ───────────────────────────────────────────────────────────────── let passed = 0; let failed = 0; function pass(label) { console.log(` PASS ${label}`); passed++; } function fail(label, reason) { console.error(` FAIL ${label}: ${reason}`); failed++; } function assert(cond, label, reason = '') { if (cond) pass(label); else fail(label, reason || 'assertion false'); } // ─── setup: tmp db ─────────────────────────────────────────────────────────── const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adr130-smoke-')); const dbPath = path.join(tmpDir, 'memory.db'); async function cleanup() { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} } // ─── test 1: schema creates graph_edges ────────────────────────────────────── console.log('\n[ADR-130 smoke] Phase 1 — graph_edges schema migration\n'); async function testSchemaCreation() { console.log('TEST 1: graph_edges table created by MEMORY_SCHEMA_V3'); try { // Dynamically import initializeMemoryDatabase const { initializeMemoryDatabase, MEMORY_SCHEMA_V3 } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/memory-initializer.js') ); // Initialize db into tmpDir const result = await initializeMemoryDatabase({ dbPath, force: true, verbose: false }); assert(result.success, '1a: initializeMemoryDatabase succeeds', JSON.stringify(result)); // Verify graph_edges exists via sql.js const SQL = await loadSqlJs(); const fileBuffer = fs.readFileSync(dbPath); const db = new SQL.Database(fileBuffer); const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='graph_edges'"); assert(tables?.[0]?.values?.length > 0, '1b: graph_edges table exists in schema'); // Verify all 4 indexes exist const idxResult = db.exec("SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_graph_edges_%'"); const indexCount = idxResult?.[0]?.values?.length ?? 0; assert(indexCount >= 4, `1c: graph_edges has 4 indexes (found ${indexCount})`); // Verify temporal columns exist const colResult = db.exec("PRAGMA table_info(graph_edges)"); const cols = colResult?.[0]?.values?.map(r => r[1]) ?? []; assert(cols.includes('confidence'), '1d: confidence column exists'); assert(cols.includes('decay_rate'), '1e: decay_rate column exists'); assert(cols.includes('last_reinforced'), '1f: last_reinforced column exists'); assert(cols.includes('witness_id'), '1g: witness_id column exists'); assert(cols.includes('embedding_ref'), '1h: embedding_ref column exists'); db.close(); } catch (err) { fail('1: schema creation', err.message); } } // ─── test 2: insert edge with embedding_ref ─────────────────────────────────── async function testEdgeInsert() { console.log('\nTEST 2: insertGraphEdge writes row with embedding_ref'); try { const { insertGraphEdge, countGraphEdges, _resetBridgeDb } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js') ); // Reset cache so it picks up our tmpDir db _resetBridgeDb(); const ok = await insertGraphEdge({ sourceId: 'agent:abc-001', targetId: 'task:xyz-002', relation: 'assigned_to', weight: 0.9, embedding: Array.from({ length: 384 }, (_, i) => Math.sin(i * 0.1)), dbPath, }); assert(ok, '2a: insertGraphEdge returns true'); const count = await countGraphEdges(dbPath); assert(count === 1, `2b: graph_edges has 1 row (found ${count})`); // Close the better-sqlite3 connection so its WAL is checkpointed back // into the main DB file before we re-read via sql.js (#2431 added WAL // mode for cross-connection safety; without this checkpoint, sql.js // sees only the pre-insert main file and the inserted rows live in // the .db-wal sidecar). _resetBridgeDb(); // Verify embedding_ref is inline-encoded const SQL = await loadSqlJs(); const fileBuffer = fs.readFileSync(dbPath); const db = new SQL.Database(fileBuffer); const rows = db.exec("SELECT embedding_ref FROM graph_edges WHERE source_id = 'agent:abc-001'"); const embRef = rows?.[0]?.values?.[0]?.[0]; assert(typeof embRef === 'string' && embRef.startsWith('inline:'), `2c: embedding_ref has inline: prefix (got ${embRef?.slice?.(0, 20)}...)`); // Verify decoding round-trip const { decodeEmbedding } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/embedding-quantization.js') ); const decoded = decodeEmbedding(embRef); assert(decoded !== null, '2d: embedding decodes without error'); assert(decoded?.length === 384, `2e: decoded embedding has 384 dims (got ${decoded?.length})`); db.close(); } catch (err) { fail('2: edge insert', err.message); } } // ─── test 3: legacy ID auto-prefix ─────────────────────────────────────────── async function testLegacyIdPrefix() { console.log('\nTEST 3: legacy unprefixed ID auto-prefixed as "mem:"'); try { const { insertGraphEdge, countGraphEdges, _resetBridgeDb } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js') ); _resetBridgeDb(); // Insert with a plain legacy ID (no domain prefix) const ok = await insertGraphEdge({ sourceId: 'mem:legacy-id-no-prefix', // already prefixed by handler targetId: 'mem:other-legacy', relation: 'followed_by', weight: 0.5, dbPath, }); assert(ok, '3a: insertGraphEdge with mem:-prefixed IDs succeeds'); // Checkpoint WAL → main DB before re-reading via sql.js (#2431). _resetBridgeDb(); // Verify row exists with the prefixed IDs const SQL = await loadSqlJs(); const fileBuffer = fs.readFileSync(dbPath); const db = new SQL.Database(fileBuffer); const rows = db.exec("SELECT source_id FROM graph_edges WHERE source_id = 'mem:legacy-id-no-prefix'"); assert(rows?.[0]?.values?.length > 0, '3b: row has source_id = "mem:legacy-id-no-prefix"'); db.close(); } catch (err) { fail('3: legacy ID prefix', err.message); } } // ─── test 4: PQ encoding/decoding invariants ────────────────────────────────── async function testPQEncoding() { console.log('\nTEST 4: PQ encoder/decoder invariants'); try { const { encodeEmbedding, decodeEmbedding, encodedByteSize } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/embedding-quantization.js') ); // Test 384-dim encoding const vec = Array.from({ length: 384 }, (_, i) => (i / 384) * 2 - 1); const encoded = encodeEmbedding(vec); assert(encoded.startsWith('inline:'), '4a: encoded starts with "inline:"'); const decoded = decodeEmbedding(encoded); assert(decoded !== null, '4b: decodes without error'); assert(decoded.length === 384, `4c: decoded length = 384 (got ${decoded?.length})`); // Cosine similarity of original vs decoded should be close to 1 let dot = 0, normA = 0, normB = 0; for (let i = 0; i < 384; i++) { dot += vec[i] * decoded[i]; normA += vec[i] * vec[i]; normB += decoded[i] * decoded[i]; } const cos = dot / (Math.sqrt(normA) * Math.sqrt(normB)); assert(cos > 0.99, `4d: cosine similarity after round-trip > 0.99 (got ${cos.toFixed(6)})`); // Storage footprint check (ADR-130: ≤500KB per 1000 edges = raw quantized bytes only) const charCount = encoded.length - 'inline:'.length; const estimatedBase64Bytes = Math.ceil(charCount * 3 / 4); // decode back to raw bytes assert(estimatedBase64Bytes < 2000, `4e: encoded blob under 2KB (got ~${estimatedBase64Bytes} bytes)`); // Raw bytes per 1000 edges should be < 500KB (ADR-130 target) // Raw payload = 4+4+4+4+dims = 400 bytes for 384-dim const rawBytesPerEdge = estimatedBase64Bytes; const per1kEdgesKB = (rawBytesPerEdge * 1000) / 1024; assert(per1kEdgesKB < 500, `4f: per-1000-edges raw payload < 500KB (got ${per1kEdgesKB.toFixed(1)} KB)`); console.log(` PQ blob: ~${rawBytesPerEdge} bytes/edge (~${per1kEdgesKB.toFixed(1)} KB/1k edges raw)`); } catch (err) { fail('4: PQ encoding', err.message); } } // ─── test 5: graph_edges table absent from old DB → auto-created ────────────── async function testTableAutoCreate() { console.log('\nTEST 5: graph_edges auto-created on old DB without it'); const oldDbPath = path.join(tmpDir, 'old-memory.db'); try { // Create a DB without graph_edges const SQL = await loadSqlJs(); const db = new SQL.Database(); db.run(`CREATE TABLE IF NOT EXISTS memory_entries (id TEXT PRIMARY KEY, key TEXT, content TEXT)`); const data = db.export(); fs.writeFileSync(oldDbPath, Buffer.from(data)); db.close(); // Now insert via graph-edge-writer — it should auto-create graph_edges const { insertGraphEdge, _resetBridgeDb } = await import( path.join(projectRoot, 'v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js') ); _resetBridgeDb(); const ok = await insertGraphEdge({ sourceId: 'entity:test', targetId: 'pattern:abc', relation: 'matched', dbPath: oldDbPath, }); assert(ok, '5a: insertGraphEdge succeeds on DB without graph_edges'); // Checkpoint WAL → main DB before re-reading via sql.js (#2431). // Without this, sql.js sees only the pre-insert main file (just // memory_entries) and the auto-created graph_edges table sits in // the .db-wal sidecar, failing assertion 5b. _resetBridgeDb(); // Verify table was created const fileBuffer = fs.readFileSync(oldDbPath); const dbCheck = new SQL.Database(fileBuffer); const tables = dbCheck.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='graph_edges'"); assert(tables?.[0]?.values?.length > 0, '5b: graph_edges auto-created on old DB'); dbCheck.close(); } catch (err) { fail('5: table auto-create', err.message); } } // ─── run all tests ──────────────────────────────────────────────────────────── try { await testSchemaCreation(); await testEdgeInsert(); await testLegacyIdPrefix(); await testPQEncoding(); await testTableAutoCreate(); } finally { await cleanup(); } console.log(`\n${'─'.repeat(50)}`); console.log(`Results: ${passed} passed, ${failed} failed`); console.log('─'.repeat(50)); if (failed > 0) { console.error('\nSmoke test FAILED — ADR-130 Phase 1 acceptance criteria not met.\n'); process.exit(1); } else { console.log('\nSmoke test PASSED — ADR-130 Phase 1 acceptance criteria met.\n'); process.exit(0); }