velesdb-memory 0.14.1

VelesDB-memory: local-first MCP memory server for AI agents (remember/recall/relate/forget/why + deterministic context compiler).
Documentation
use std::path::PathBuf;
use std::time::Duration;

use super::job_state::{JobPhase, JobRecord, JobSpec, JobStore};
use crate::mutation::catchup::CatchUpConfig;
use crate::mutation::controller::ControllerConfig;
use crate::mutation::journal::EpochIdentity;

const EPOCH: &str = "0123456789abcdef0123456789abcdef";

#[test]
fn durable_job_round_trips_its_complete_resume_contract() {
    let root = tempfile::tempdir().expect("root");
    let workspace = root.path().join("workspace");
    std::fs::create_dir(&workspace).expect("workspace");
    let record = record(root.path());

    let store = JobStore::create(&workspace, &record).expect("create job");
    let loaded = store.load().expect("load job");

    assert_eq!(loaded, record);
}

#[test]
fn future_job_version_is_refused_instead_of_guessed() {
    let root = tempfile::tempdir().expect("root");
    let workspace = root.path().join("workspace");
    std::fs::create_dir(&workspace).expect("workspace");
    let record = record(root.path());
    let store = JobStore::create(&workspace, &record).expect("create job");
    let path = workspace.join("online-migration-job.json");
    let mut value: serde_json::Value =
        serde_json::from_slice(&std::fs::read(&path).expect("read state")).expect("json");
    value["version"] = serde_json::json!(99);
    std::fs::write(&path, serde_json::to_vec(&value).expect("encode")).expect("write future");

    let error = store.load().expect_err("future version must refuse");

    assert!(error.to_string().contains("version"), "{error}");
}

#[test]
fn phase_machine_allows_deadline_reopen_but_refuses_unsafe_rollback() {
    let root = tempfile::tempdir().expect("root");
    let mut record = record(root.path());
    for phase in [
        JobPhase::Capturing,
        JobPhase::BaseCopied,
        JobPhase::CatchingUp,
        JobPhase::CutoverReady,
        JobPhase::Quiescing,
        JobPhase::CatchingUp,
    ] {
        record.transition(phase).expect("valid transition");
    }
    record
        .transition(JobPhase::CutoverReady)
        .expect("ready again");
    record
        .transition(JobPhase::Quiescing)
        .expect("quiescing again");
    record.transition(JobPhase::Activated).expect("activate");

    let error = record
        .transition(JobPhase::Cancelled)
        .expect_err("activated job cannot cancel");

    assert!(error.to_string().contains("transition"), "{error}");
}

#[test]
fn pre_quiescing_job_can_cancel_but_cannot_restart() {
    let root = tempfile::tempdir().expect("root");
    let mut record = record(root.path());
    record.transition(JobPhase::Capturing).expect("capture");
    record
        .transition(JobPhase::Cancelled)
        .expect("cancel source-authoritative job");

    let error = record
        .transition(JobPhase::CatchingUp)
        .expect_err("terminal cancellation");

    assert!(error.to_string().contains("transition"), "{error}");
}

fn record(root: &std::path::Path) -> JobRecord {
    let source = root.join("source");
    let destination = root.join("destination");
    let identity = EpochIdentity::for_test(
        source,
        "source-model",
        "target-model",
        3,
        &format!("sha256:{}", "ab".repeat(32)),
        destination,
        EPOCH,
    );
    JobRecord::new(JobSpec {
        identity,
        target_backend: "hash".to_owned(),
        journal_max_bytes: 1_048_576,
        catch_up: CatchUpConfig {
            fact_batch: 64,
            replay_batch: 64,
            edge_cap: 64,
        },
        controller: ControllerConfig {
            observation_window: 3,
            pause_budget: Duration::from_secs(1),
            verification_reserve: Duration::from_millis(50),
        },
        workspace: PathBuf::from("workspace"),
    })
    .expect("record")
}