remem-ai 0.6.39

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::fs;
#[cfg(unix)]
use std::fs::File;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use super::{check_plaintext_artifacts_in, Status, SQLITE_PLAINTEXT_HEADER};

fn arbitrary_backup_test_dir() -> PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "remem-plaintext-arbitrary-unreadable-{}-{}",
        std::process::id(),
        chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
    ));
    fs::create_dir_all(&dir).expect("test directory should create");
    dir
}

fn write_plaintext_db(path: &Path) {
    let mut content = SQLITE_PLAINTEXT_HEADER.to_vec();
    content.extend_from_slice(&[0_u8; 32]);
    fs::write(path, content).expect("plaintext fixture should write");
}

fn write_non_plaintext_db(path: &Path) {
    fs::write(path, [0xAB_u8; 64]).expect("non-plaintext fixture should write");
}

#[cfg(unix)]
#[test]
fn permission_restricted_arbitrary_backup_never_reports_ok() {
    let dir = arbitrary_backup_test_dir();
    let db_path = dir.join("remem.db");
    fs::write(&db_path, [0xAB_u8; 64]).expect("encrypted-header fixture should write");
    let backup = dir.join("pre-encryption-backup");
    let mut plaintext = SQLITE_PLAINTEXT_HEADER.to_vec();
    plaintext.extend_from_slice(&[0_u8; 32]);
    fs::write(&backup, plaintext).expect("plaintext backup fixture should write");
    fs::set_permissions(&backup, fs::Permissions::from_mode(0o000))
        .expect("backup should become unreadable");
    let access_denied = File::open(&backup).is_err();

    let result = check_plaintext_artifacts_in(&dir, &db_path, true);

    assert_eq!(
        result.status,
        if access_denied {
            Status::Warn
        } else {
            Status::Fail
        }
    );
    assert!(result.detail.contains("pre-encryption-backup"));
    if access_denied {
        assert!(result.detail.contains("cannot open"));
    } else {
        assert!(result.detail.contains("confirmed plaintext"));
    }
    fs::set_permissions(&backup, fs::Permissions::from_mode(0o600))
        .expect("backup permissions should restore");
    fs::remove_dir_all(&dir).ok();
}

#[test]
fn root_content_scan_reports_candidates_without_key_or_log_noise() {
    let dir = arbitrary_backup_test_dir();
    let db_path = dir.join("remem.db");
    write_non_plaintext_db(&db_path);
    write_plaintext_db(&dir.join("pre-encryption-backup"));
    fs::write(dir.join("truncated.sqlite"), b"short").expect("short fixture should write");
    fs::write(dir.join(".key"), b"short").expect("key fixture should write");
    fs::write(dir.join("remem.log"), b"short").expect("log fixture should write");
    write_non_plaintext_db(&dir.join("unrelated.txt"));
    write_non_plaintext_db(&dir.join("noise.sqlite"));
    #[cfg(unix)]
    let unreadable = {
        let path = dir.join("unreadable.sqlite");
        write_plaintext_db(&path);
        fs::set_permissions(&path, fs::Permissions::from_mode(0o000))
            .expect("fixture permissions should change");
        let access_denied = File::open(&path).is_err();
        (path, access_denied)
    };

    let result = check_plaintext_artifacts_in(&dir, &db_path, true);

    assert_eq!(result.status, Status::Fail);
    for expected in ["pre-encryption-backup", "truncated.sqlite", "shorter than"] {
        assert!(result.detail.contains(expected), "{}", result.detail);
    }
    #[cfg(unix)]
    if unreadable.1 {
        assert!(result.detail.contains("cannot open"));
        assert!(result.detail.contains("unreadable.sqlite"));
    } else {
        assert!(result.detail.contains("unreadable.sqlite"));
    }
    for excluded in [".key", "remem.log", "unrelated.txt", "noise.sqlite"] {
        assert!(!result.detail.contains(excluded), "{}", result.detail);
    }
    #[cfg(unix)]
    fs::set_permissions(unreadable.0, fs::Permissions::from_mode(0o600))
        .expect("fixture permissions should restore");
    fs::remove_dir_all(&dir).ok();
}

#[test]
fn plaintext_live_db_remediation_orders_encryption_backup_and_disposal() {
    let dir = arbitrary_backup_test_dir();
    let db_path = dir.join("remem.db");
    for name in ["remem.db", "remem.db.bak", "remem.db.enc"] {
        write_plaintext_db(&dir.join(name));
    }

    let result = check_plaintext_artifacts_in(&dir, &db_path, true);

    assert_eq!(result.status, Status::Warn);
    let steps = [
        "`REMEM_DATA_DIR/remem.db.bak`",
        "`REMEM_DATA_DIR/remem.db.enc`",
        "`REMEM_DATA_DIR/backups/`",
        "real directory and not a symbolic link",
        "distinct unused destination name",
        "without overwriting any existing file",
        "`remem encrypt`",
        "`remem status`",
        "`remem doctor`",
        "`remem admin backup`",
        "manually delete",
    ]
    .map(|step| result.detail.find(step).expect("ordered remediation step"));
    assert!(steps.windows(2).all(|pair| pair[0] < pair[1]));
    assert!(result
        .detail
        .contains("rerun `remem doctor` until Plaintext residue passes"));
    for shell_fragment in ["export ", "mkdir -p", "mktemp", "mv \"", "[ -L "] {
        assert!(!result.detail.contains(shell_fragment), "{}", result.detail);
    }
    fs::remove_dir_all(&dir).ok();
}

#[test]
fn short_named_artifact_keeps_inspection_incomplete() {
    let dir = arbitrary_backup_test_dir();
    let db_path = dir.join("remem.db");
    fs::write(&db_path, [0xAB_u8; 64]).expect("encrypted-header fixture should write");
    fs::write(dir.join("remem.db.bak"), b"short").expect("short fixture should write");

    let result = check_plaintext_artifacts_in(&dir, &db_path, true);

    assert_eq!(result.status, Status::Warn);
    assert!(result.detail.contains("shorter than the SQLite header"));
    fs::remove_dir_all(&dir).ok();
}