lix 0.17.1

Embeddable version control for apps and AI agents.
Documentation
use super::*;
use crate::sync::SyncTransportFuture;
use crate::sync::http::{HttpSyncTransport, RawHttpClient, RawHttpRequest, RawHttpResponse};
use crate::sync::runtime::SyncShutdown;

#[derive(Clone)]
struct UnusedClient;

impl RawHttpClient for UnusedClient {
    fn send(&self, _: RawHttpRequest) -> SyncTransportFuture<'_, RawHttpResponse> {
        panic!("incompatible handshake must not produce an admitted transport")
    }
}

#[tokio::test]
async fn terminal_sync_mismatch_blocks_warm_sql_and_preserves_pending_edits_on_reopen() {
    let mismatch = crate::sync::sync_server_protocol_mismatch(Some(13));
    assert!(mismatch.message.contains("upgrade"));
    terminal_error_preserves_pending_edits(mismatch).await;
}

#[tokio::test]
async fn terminal_missing_incorporation_proof_preserves_pending_edits_on_reopen() {
    terminal_error_preserves_pending_edits(
        LixError::new(
            "LIX_PARTIAL_MERGE_PROOF_UNAVAILABLE",
            "legacy checkpoint incorporation provenance is unavailable",
        )
        .with_hint("retain replica storage; reconciliation needs a provable incorporation path, and fetching row payloads alone cannot restore erased legacy provenance"),
    )
    .await;
}

async fn terminal_error_preserves_pending_edits(mismatch: LixError) {
    let (authority, engine, session, state) = fixture().await;
    let storage = engine.storage();
    let select = "SELECT value FROM lix_key_value WHERE key='resident'";
    execute_hydrating(
        &session,
        &storage,
        &state,
        &authority,
        select,
        &[],
        &mut Fetches::default(),
    )
    .await
    .unwrap();
    execute_hydrating(
        &session,
        &storage,
        &state,
        &authority,
        "UPDATE lix_key_value SET value='pending' WHERE key='resident'",
        &[],
        &mut Fetches::default(),
    )
    .await
    .unwrap();

    let (_shutdown, shutdown_rx) = tokio::sync::watch::channel(SyncShutdown::Running);
    let (_demand, demand_rx) = tokio::sync::mpsc::channel(1);
    let expected = mismatch.clone();
    let result = tokio::time::timeout(
        std::time::Duration::from_secs(5),
        crate::sync::partial_runtime::run_partial_worker_with_engine(
            storage.clone(),
            state.clone(),
            None::<HttpSyncTransport<UnusedClient>>,
            move || {
                let error = mismatch.clone();
                Box::pin(async move { Err(error) })
            },
            shutdown_rx,
            demand_rx,
            Some(engine.sync_mode().change_watcher()),
            Some(engine.clone()),
        ),
    )
    .await
    .expect("terminal mismatch must stop instead of retrying")
    .unwrap_err();
    assert_eq!(result.code, expected.code);
    for sql in [
        select,
        "UPDATE lix_key_value SET value='blocked' WHERE key='resident'",
    ] {
        let error = session.execute(sql, &[]).await.unwrap_err();
        assert_eq!(error.code, expected.code);
        assert_eq!(error.message, expected.message);
        assert_eq!(error.hint, expected.hint);
    }

    drop(session);
    drop(engine);
    let (reopened, fresh) = Engine::new_partial_replica(storage, EngineOptions::new(), &state)
        .await
        .unwrap();
    reopened
        .sync_mode()
        .admit_partial_replica(state, crate::sync::partial_replica_write_capability());
    assert!(value(fresh.execute(select, &[]).await.unwrap()).contains("pending"));
    assert!(value(authority.execute(select, &[]).await.unwrap()).contains("before"));
}