eidetic-engine 0.17.0

Durable, local-first, explainable memory for coding agents.
use super::*;
use crate::db::{CreateMemoryInput, CreateMemoryLinkInput, CreateWorkspaceInput, MemoryLinkSource};
use crate::models::WorkspaceId;

fn fixture(count: usize) -> (tempfile::TempDir, DbConnection, Vec<String>) {
    let root = tempfile::tempdir().expect("temporary real store");
    let connection = DbConnection::open_file(&root.path().join("ask.db")).expect("open store");
    connection.migrate().expect("migrate real schema");
    let workspace_id = WorkspaceId::from_uuid(uuid::Uuid::from_bytes([31; 16])).to_string();
    connection
        .insert_workspace(
            &workspace_id,
            &CreateWorkspaceInput {
                path: root.path().to_string_lossy().into_owned(),
                name: Some("ask storage regression".to_owned()),
            },
        )
        .expect("workspace");
    let ids: Vec<_> = (0..count).map(|index| format!("mem_{index:026}")).collect();
    for (index, id) in ids.iter().enumerate() {
        connection
            .insert_memory(
                id,
                &CreateMemoryInput {
                    workspace_id: workspace_id.clone(),
                    level: "semantic".to_owned(),
                    kind: "fact".to_owned(),
                    content: format!("Deployment setting token_{index}."),
                    workflow_id: None,
                    confidence: 1.0,
                    utility: 0.5,
                    importance: 0.5,
                    provenance_uri: Some(format!("manual://ask-storage/{index}")),
                    trust_class: "human_explicit".to_owned(),
                    trust_subclass: None,
                    tags: Vec::new(),
                    valid_from: None,
                    valid_to: None,
                },
            )
            .expect("memory");
    }
    (root, connection, ids)
}

fn link(
    connection: &DbConnection,
    number: usize,
    source: &str,
    target: &str,
    relation: MemoryLinkRelation,
) {
    connection
        .insert_memory_link(
            &format!("link_{number:026}"),
            &CreateMemoryLinkInput {
                src_memory_id: source.to_owned(),
                dst_memory_id: target.to_owned(),
                relation,
                weight: 1.0,
                confidence: 0.9,
                directed: true,
                evidence_count: 1,
                last_reinforced_at: None,
                source: MemoryLinkSource::Agent,
                created_by: Some("ask-storage-test".to_owned()),
                metadata_json: None,
            },
        )
        .expect("stored link");
}

#[test]
fn late_and_cross_batch_edges_are_loaded_once_in_stable_order() {
    let (_root, connection, ids) = fixture(LINK_QUERY_BATCH_SIZE * 2 + 7);
    link(
        &connection,
        1,
        &ids[0],
        &ids[LINK_QUERY_BATCH_SIZE],
        MemoryLinkRelation::Contradicts,
    );
    link(
        &connection,
        2,
        &ids[LINK_QUERY_BATCH_SIZE * 2],
        &ids[LINK_QUERY_BATCH_SIZE * 2 + 1],
        MemoryLinkRelation::Contradicts,
    );
    link(
        &connection,
        3,
        &ids[1],
        &ids[2],
        MemoryLinkRelation::Related,
    );
    let mut scope: Vec<_> = ids.iter().map(String::as_str).collect();
    let before = connection.list_audit_entries(None, None).expect("audits");
    let actual = load_scoped_contradictions(&connection, &scope).expect("complete link scan");
    assert_eq!(actual.len(), 2);
    assert_eq!(actual[0].id, format!("link_{:026}", 1));
    assert_eq!(actual[1].id, format!("link_{:026}", 2));
    assert_eq!(actual[1].src_memory_id, ids[LINK_QUERY_BATCH_SIZE * 2]);
    assert_eq!(actual[1].confidence, 0.9);
    assert_eq!(actual[1].source, "agent");
    scope.reverse();
    scope.push(ids[0].as_str());
    let reversed = load_scoped_contradictions(&connection, &scope).expect("permuted scope");
    assert_eq!(
        actual.iter().map(|link| &link.id).collect::<Vec<_>>(),
        reversed.iter().map(|link| &link.id).collect::<Vec<_>>()
    );
    assert_eq!(
        before.len(),
        connection
            .list_audit_entries(None, None)
            .expect("audits after reads")
            .len()
    );
}

#[test]
fn an_incident_link_cannot_reintroduce_an_excluded_memory() {
    let (_root, connection, ids) = fixture(3);
    link(
        &connection,
        1,
        &ids[0],
        &ids[1],
        MemoryLinkRelation::Contradicts,
    );
    link(
        &connection,
        2,
        &ids[0],
        &ids[2],
        MemoryLinkRelation::Contradicts,
    );
    let links = load_scoped_contradictions(&connection, &[&ids[0], &ids[1]]).expect("scoped links");
    assert_eq!(links.len(), 1);
    assert_eq!(links[0].dst_memory_id, ids[1]);
}

#[test]
fn empty_scope_needs_no_link_table_but_missing_storage_for_a_real_scope_fails() {
    let root = tempfile::tempdir().expect("temporary database");
    let connection = DbConnection::open_file(&root.path().join("unmigrated.db")).expect("open");
    assert!(
        load_scoped_contradictions(&connection, &[])
            .expect("empty scope")
            .is_empty()
    );
    assert!(matches!(
        load_scoped_contradictions(&connection, &["mem_00000000000000000000000001"]),
        Err(DomainError::Storage { .. })
    ));
}