hippmem-engine 0.1.0

HIPPMEM native association memory engine — unified public Rust API orchestration layer
Documentation
//! acceptance test: traverse API.
//!
//! Tests BFS graph traversal: depth control, direction filtering, cycle protection, NotFound.

use hippmem_core::model::enums::ContentType;
use hippmem_core::model::unit::WriteContext;
use hippmem_engine::{Engine, EngineConfig, WriteMemoryInput};
use tempfile::tempdir;

fn ctx() -> WriteContext {
    WriteContext {
        conversation_id: Some(1),
        session_id: Some(1),
        project_id: None,
        task_id: None,
        user_id: None,
        local_time: hippmem_core::time::Timestamp(1_700_000_000_000),
        preceding_memory_ids: vec![],
        source_refs: vec![],
    }
}

#[test]
fn traverse_depth_one_finds_neighbors() {
    let dir = tempdir().unwrap();
    let engine = Engine::open(EngineConfig {
        store_dir: dir.path().join("hippmem.redb"),
        ..Default::default()
    })
    .unwrap();

    // Write 2 memories sharing keywords (same verified pattern as explain_inspect)
    let out1 = engine
        .write(WriteMemoryInput {
            content: "The project uses Rust to develop backend database services".into(),
            content_type: Some(ContentType::ProjectKnowledge),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();
    engine
        .write(WriteMemoryInput {
            content: "Building a high-performance database query engine with Rust".into(),
            content_type: Some(ContentType::Decision),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();

    let output = engine
        .traverse(hippmem_engine::TraverseInput::new(out1.memory_id))
        .unwrap();

    // Verify output structure is complete (no panic)
    // Node count depends on the deterministic backend's association edge-building result (≥0)
    for node in &output.nodes {
        assert!(node.depth >= 1, "depth should start at 1");
        assert!(!node.content_preview.is_empty());
    }
    // No duplicate nodes
    let ids: std::collections::HashSet<_> = output.nodes.iter().map(|n| n.id).collect();
    assert_eq!(
        ids.len(),
        output.nodes.len(),
        "should have no duplicate nodes"
    );

    engine.close().unwrap();
}

#[test]
fn traverse_depth_two_reaches_further() {
    let dir = tempdir().unwrap();
    let engine = Engine::open(EngineConfig {
        store_dir: dir.path().join("hippmem.redb"),
        ..Default::default()
    })
    .unwrap();

    // Write chain-linked memories: A → B → C
    // A: Rust + database
    // B: database + query
    // C: query + performance
    let out_a = engine
        .write(WriteMemoryInput {
            content: "The project uses Rust to develop a backend database".into(),
            content_type: Some(ContentType::ProjectKnowledge),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();
    let _out_b = engine
        .write(WriteMemoryInput {
            content: "Database query engine optimization plan".into(),
            content_type: Some(ContentType::Decision),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();
    let _out_c = engine
        .write(WriteMemoryInput {
            content: "Query performance analysis tool development".into(),
            content_type: Some(ContentType::UserStatement),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();

    let output = engine
        .traverse(hippmem_engine::TraverseInput {
            start_id: out_a.memory_id,
            max_depth: 2,
            direction: hippmem_engine::TraverseDirection::Outgoing,
            link_types: None,
        })
        .unwrap();

    // Verify depth-2 traversal does not panic and node depths are reasonable
    for node in &output.nodes {
        assert!(node.depth == 1 || node.depth == 2, "depth should be 1 or 2");
    }

    engine.close().unwrap();
}

#[test]
fn traverse_not_found_errors() {
    let dir = tempdir().unwrap();
    let engine = Engine::open(EngineConfig {
        store_dir: dir.path().join("hippmem.redb"),
        ..Default::default()
    })
    .unwrap();

    let result = engine.traverse(hippmem_engine::TraverseInput::new(
        hippmem_core::ids::MemoryId(99999),
    ));
    assert!(result.is_err());

    engine.close().unwrap();
}

#[test]
fn traverse_no_cycle_infinite_loop() {
    let dir = tempdir().unwrap();
    let engine = Engine::open(EngineConfig {
        store_dir: dir.path().join("hippmem.redb"),
        ..Default::default()
    })
    .unwrap();

    // Write 2 mutually-associated memories (A ↔ B)
    let out_a = engine
        .write(WriteMemoryInput {
            content: "Using Rust to develop databases".into(),
            content_type: Some(ContentType::ProjectKnowledge),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();
    let _out_b = engine
        .write(WriteMemoryInput {
            content: "Databases must be written in Rust".into(),
            content_type: Some(ContentType::Decision),
            context: ctx(),
            importance_hint: Some(0.7),
            source_refs: vec![],
        })
        .unwrap();

    let output = engine
        .traverse(hippmem_engine::TraverseInput {
            start_id: out_a.memory_id,
            max_depth: 5,
            direction: hippmem_engine::TraverseDirection::Outgoing,
            link_types: None,
        })
        .unwrap();

    // Should not loop infinitely: visited set prevents revisits on exit
    // Node count should be finite (visit each memory at most once)
    let unique_ids: std::collections::HashSet<_> = output.nodes.iter().map(|n| n.id).collect();
    assert_eq!(
        unique_ids.len(),
        output.nodes.len(),
        "should have no duplicate nodes (cycle protection)"
    );

    engine.close().unwrap();
}

#[test]
fn traverse_max_depth_clamped() {
    let dir = tempdir().unwrap();
    let engine = Engine::open(EngineConfig {
        store_dir: dir.path().join("hippmem.redb"),
        ..Default::default()
    })
    .unwrap();

    engine
        .write(WriteMemoryInput {
            content: "Rust database".into(),
            content_type: Some(ContentType::ProjectKnowledge),
            context: ctx(),
            importance_hint: None,
            source_refs: vec![],
        })
        .unwrap();

    // depth=0 should be clamped to 1
    let output = engine
        .traverse(hippmem_engine::TraverseInput {
            start_id: hippmem_core::ids::MemoryId(1),
            max_depth: 0,
            ..Default::default() // won't be used because start_id overrides
        })
        .unwrap_or_else(|_| {
            // NotFound is OK — we're testing the clamp
            hippmem_engine::TraverseOutput {
                nodes: vec![],
                edges: vec![],
            }
        });

    // Verify no panic
    let _ = output.nodes.len();

    engine.close().unwrap();
}