#!/usr/bin/env bash
# ============================================================================
# brain_restore_test.sh — Verify integrity of a Vincent AI brain backup
#
# Runs a battery of checks against a backup directory without modifying it.
#
# Usage: brain_restore_test.sh <backup_directory>
#
# Checks:
# 1. Required files exist
# 2. events.db integrity via SQLite PRAGMA
# 3. Entity count in reasonable range
# 4. Python can import and query the restored DB
# 5. Manifest checksums are valid
#
# Exit code: 0 = all pass, 1 = one or more failures
# ============================================================================
set -euo pipefail
# ── Color output ─────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
pass() {
PASS_COUNT=$((PASS_COUNT + 1))
echo -e " ${GREEN}✓ PASS${NC} $1"
}
fail() {
FAIL_COUNT=$((FAIL_COUNT + 1))
echo -e " ${RED}✗ FAIL${NC} $1"
}
skip() {
SKIP_COUNT=$((SKIP_COUNT + 1))
echo -e " ${YELLOW}⊘ SKIP${NC} $1"
}
# ── Argument validation ──────────────────────────────────────────────────────
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <backup_directory>"
echo "Example: $0 ~/backups/brain/2026-06-02"
exit 1
fi
BACKUP_DIR="$1"
echo "═══════════════════════════════════════════════════════"
echo " Brain Backup Restore Test"
echo " Backup: $BACKUP_DIR"
echo " Date: $(date '+%Y-%m-%d %H:%M:%S')"
echo "═══════════════════════════════════════════════════════"
echo ""
# ── Pre-flight ───────────────────────────────────────────────────────────────
if [[ ! -d "$BACKUP_DIR" ]]; then
fail "Backup directory does not exist: $BACKUP_DIR"
echo ""
echo "═══════════════════════════════════════════════════════"
echo " RESULT: $PASS_COUNT passed, $FAIL_COUNT failed, $SKIP_COUNT skipped"
echo "═══════════════════════════════════════════════════════"
exit 1
fi
# ── Test 1: Required files exist ─────────────────────────────────────────────
echo "--- Test 1: Required files present ---"
REQUIRED_FILES=("events.db" "config.py" "brain.py")
for f in "${REQUIRED_FILES[@]}"; do
if [[ -f "$BACKUP_DIR/$f" ]]; then
pass "$f exists ($(du -h "$BACKUP_DIR/$f" | cut -f1))"
else
fail "$f not found"
fi
done
# Optional files (config extras)
OPTIONAL_FILES=("embeddings.py" "storage.py")
for f in "${OPTIONAL_FILES[@]}"; do
if [[ -f "$BACKUP_DIR/$f" ]]; then
pass "$f exists (optional)"
else
skip "$f not found (optional)"
fi
done
# Directories
if [[ -d "$BACKUP_DIR/events" ]]; then
event_files=$(find "$BACKUP_DIR/events" -type f -name '*.json' 2>/dev/null | wc -l)
pass "events/ directory exists ($event_files JSON files)"
else
skip "events/ directory not found"
fi
if [[ -d "$BACKUP_DIR/migrations" ]]; then
mig_files=$(find "$BACKUP_DIR/migrations" -type f 2>/dev/null | wc -l)
pass "migrations/ directory exists ($mig_files files)"
else
skip "migrations/ directory not found"
fi
if [[ -f "$BACKUP_DIR/manifest.txt" ]]; then
pass "manifest.txt exists"
else
skip "manifest.txt not found"
fi
echo ""
# ── Test 2: SQLite integrity check ───────────────────────────────────────────
echo "--- Test 2: SQLite database integrity ---"
DB="$BACKUP_DIR/events.db"
if [[ -f "$DB" ]]; then
integrity=$(sqlite3 "$DB" "PRAGMA integrity_check;" 2>&1)
if [[ "$integrity" == "ok" ]]; then
pass "PRAGMA integrity_check returned 'ok'"
else
fail "PRAGMA integrity_check returned: $integrity"
fi
# Check WAL mode status
wal_mode=$(sqlite3 "$DB" "PRAGMA journal_mode;" 2>&1)
if [[ $? -eq 0 ]]; then
pass "Journal mode readable: $wal_mode"
else
fail "Cannot read journal mode"
fi
# Check free pages
free_pages=$(sqlite3 "$DB" "PRAGMA freelist_count;" 2>&1)
if [[ $? -eq 0 ]]; then
pass "Free pages count: $free_pages"
else
fail "Cannot read freelist count"
fi
else
fail "events.db not found — cannot run integrity checks"
fi
echo ""
# ── Test 3: Entity count validation ──────────────────────────────────────────
echo "--- Test 3: Entity count in expected range ---"
if [[ -f "$DB" ]]; then
# Count total rows across key tables
total_entities=0
tables_found=0
# Check common brain schema tables
for table in entities conversations contexts skills prompts; do
exists=$(sqlite3 "$DB" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='$table';" 2>/dev/null || echo "0")
if [[ "$exists" -gt 0 ]]; then
count=$(sqlite3 "$DB" "SELECT count(*) FROM $table;" 2>/dev/null || echo "0")
echo " Table '$table': $count rows"
total_entities=$((total_entities + count))
tables_found=$((tables_found + 1))
fi
done
if [[ $tables_found -gt 0 ]]; then
pass "Found $tables_found tables, $total_entities total entities"
# Sanity: should have at least some data in a real backup
if [[ $total_entities -gt 0 ]]; then
pass "Entity count ($total_entities) is above zero — data present"
else
fail "Entity count is 0 — backup may be empty"
fi
else
# If no known tables, just check that the DB has any tables at all
table_list=$(sqlite3 "$DB" ".tables" 2>/dev/null || echo "")
if [[ -n "$table_list" ]]; then
pass "Database has tables: $table_list"
else
skip "No recognizable tables in database"
fi
fi
else
fail "events.db not found — cannot count entities"
fi
echo ""
# ── Test 4: Python import test ───────────────────────────────────────────────
echo "--- Test 4: Python can query restored database ---"
python_result=$(python3 -c "
import sqlite3, sys, os
db = sys.argv[1]
if not os.path.isfile(db):
print('ERROR: database file not found')
sys.exit(1)
try:
conn = sqlite3.connect(db)
cursor = conn.cursor()
# Test basic read — list all tables
cursor.execute(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;\")
tables = [row[0] for row in cursor.fetchall()]
# Test a SELECT on first table if one exists
errors = []
if tables:
for tbl in tables[:3]: # Check first 3 tables
try:
cursor.execute(f'SELECT count(*) FROM [{tbl}]')
count = cursor.fetchone()[0]
print(f' OK: [{tbl}] has {count} rows')
except Exception as e:
errors.append(f'{tbl}: {e}')
if errors:
for e in errors:
print(f' WARN: {e}')
conn.close()
print('SUCCESS')
except Exception as e:
print(f'ERROR: {e}')
sys.exit(1)
" "$DB" 2>&1)
if echo "$python_result" | grep -q "SUCCESS"; then
pass "Python successfully connected and queried the database"
# Print table details indented
echo "$python_result" | grep -E "^\s+(OK|WARN)" | sed 's/^/ /'
else
fail "Python import/query test failed: $python_result"
fi
echo ""
# ── Test 5: Manifest checksum verification ───────────────────────────────────
echo "--- Test 5: Manifest checksum verification ---"
MANIFEST="$BACKUP_DIR/manifest.txt"
if [[ -f "$MANIFEST" ]]; then
# Checksums in manifest are absolute paths; we need to verify from backup dir
# Extract just the sha256sum lines and verify
checksum_errors=0
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^# ]] && continue
[[ -z "$line" ]] && continue
# Lines that don't look like sha256sum output: 64-char hex filename
if [[ "$line" =~ ^[a-f0-9]{64}\ ]]; then
file=$(echo "$line" | awk '{print $2}')
if [[ -f "$file" ]]; then
expected=$(echo "$line" | awk '{print $1}')
actual=$(sha256sum "$file" | awk '{print $1}')
if [[ "$expected" == "$actual" ]]; then
: # checksum OK — no need to report each one
else
fail "Checksum mismatch: $(basename "$file")"
checksum_errors=$((checksum_errors + 1))
fi
fi
fi
done < "$MANIFEST"
if [[ $checksum_errors -eq 0 ]]; then
# Count how many checksums we verified
checksum_count=$(grep -cE '^[a-f0-9]{64}\ ' "$MANIFEST" 2>/dev/null || echo "0")
pass "All $checksum_count checksums verified successfully"
else
fail "$checksum_errors checksum(s) failed verification"
fi
else
skip "manifest.txt not found — skipping checksum verification"
fi
echo ""
# ── Summary ──────────────────────────────────────────────────────────────────
echo "═══════════════════════════════════════════════════════"
if [[ $FAIL_COUNT -eq 0 ]]; then
echo -e " ${GREEN}ALL CHECKS PASSED${NC}"
else
echo -e " ${RED}$FAIL_COUNT CHECK(S) FAILED${NC}"
fi
echo " Total: $PASS_COUNT passed, $FAIL_COUNT failed, $SKIP_COUNT skipped"
echo "═══════════════════════════════════════════════════════"
# Confirm backup was not modified
echo ""
echo "Backup directory was read-only during testing. No files modified."
exit $FAIL_COUNT