use super::*;
use rusqlite::params;
use rusqlite::Connection;
fn setup_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE memories (
id INTEGER PRIMARY KEY,
namespace TEXT NOT NULL,
deleted_at TEXT
);
CREATE TABLE memory_entities (
memory_id INTEGER NOT NULL,
entity_id INTEGER NOT NULL
);
CREATE TABLE relationships (
source_id INTEGER NOT NULL,
target_id INTEGER NOT NULL,
relation TEXT NOT NULL DEFAULT 'related',
weight REAL NOT NULL,
namespace TEXT NOT NULL
);",
)
.unwrap();
conn
}
fn insert_memory(conn: &Connection, id: i64, namespace: &str, deleted: bool) {
conn.execute(
"INSERT INTO memories (id, namespace, deleted_at) VALUES (?1, ?2, ?3)",
params![
id,
namespace,
if deleted { Some("2024-01-01") } else { None }
],
)
.unwrap();
}
fn link_memory_entity(conn: &Connection, memory_id: i64, entity_id: i64) {
conn.execute(
"INSERT INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
params![memory_id, entity_id],
)
.unwrap();
}
fn insert_relationship(conn: &Connection, src: i64, tgt: i64, weight: f64, ns: &str) {
conn.execute(
"INSERT INTO relationships (source_id, target_id, relation, weight, namespace)
VALUES (?1, ?2, 'related', ?3, ?4)",
params![src, tgt, weight, ns],
)
.unwrap();
}
fn reached(
conn: &Connection,
seeds: &[i64],
namespace: &str,
min_weight: f64,
max_hops: u32,
) -> Vec<i64> {
let mut ids: Vec<i64> =
traverse_from_memories_with_hops(conn, seeds, namespace, min_weight, max_hops)
.unwrap()
.into_iter()
.map(|(id, _)| id)
.collect();
ids.sort_unstable();
ids
}
#[test]
fn returns_empty_when_seeds_empty() {
let conn = setup_db();
assert!(reached(&conn, &[], "ns", 0.5, 3).is_empty());
}
#[test]
fn returns_empty_when_max_hops_zero() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
assert!(reached(&conn, &[1], "ns", 0.5, 0).is_empty());
}
#[test]
fn returns_empty_when_seed_has_no_entities() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
assert!(reached(&conn, &[1], "ns", 0.5, 3).is_empty());
}
#[test]
fn returns_empty_when_no_relationships() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
assert!(reached(&conn, &[1], "ns", 0.5, 3).is_empty());
}
#[test]
fn traversal_basic_one_hop() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 1.0, "ns");
let result = traverse_from_memories_with_hops(&conn, &[1], "ns", 0.5, 1).unwrap();
assert_eq!(result, vec![(2, 1)]);
}
#[test]
fn traversal_two_hops() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_memory(&conn, 3, "ns", false);
link_memory_entity(&conn, 3, 30);
insert_relationship(&conn, 10, 20, 1.0, "ns");
insert_relationship(&conn, 20, 30, 1.0, "ns");
let result = traverse_from_memories_with_hops(&conn, &[1], "ns", 0.5, 2).unwrap();
assert_eq!(result, vec![(2, 1), (3, 2)]);
}
#[test]
fn max_hops_limits_depth() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_memory(&conn, 3, "ns", false);
link_memory_entity(&conn, 3, 30);
insert_relationship(&conn, 10, 20, 1.0, "ns");
insert_relationship(&conn, 20, 30, 1.0, "ns");
let result = reached(&conn, &[1], "ns", 0.5, 1);
assert_eq!(result, vec![2]);
assert!(!result.contains(&3));
}
#[test]
fn relationship_with_weight_below_min_ignored() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 0.3, "ns");
assert!(reached(&conn, &[1], "ns", 0.5, 3).is_empty());
}
#[test]
fn relationship_with_weight_exactly_at_min_included() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 0.5, "ns");
assert_eq!(reached(&conn, &[1], "ns", 0.5, 1), vec![2]);
}
#[test]
fn relationship_from_different_namespace_ignored() {
let conn = setup_db();
insert_memory(&conn, 1, "ns_a", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns_a", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 1.0, "ns_b");
assert!(reached(&conn, &[1], "ns_a", 0.5, 3).is_empty());
}
#[test]
fn seeds_do_not_appear_in_result() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 1.0, "ns");
insert_relationship(&conn, 20, 10, 1.0, "ns");
let result = reached(&conn, &[1], "ns", 0.5, 3);
assert!(!result.contains(&1));
assert_eq!(result, vec![2]);
}
#[test]
fn deleted_memories_not_included() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", true);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 1.0, "ns");
assert!(reached(&conn, &[1], "ns", 0.5, 3).is_empty());
}
#[test]
fn multiple_seeds_merged_in_result() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_memory(&conn, 3, "ns", false);
link_memory_entity(&conn, 3, 30);
insert_memory(&conn, 4, "ns", false);
link_memory_entity(&conn, 4, 40);
insert_relationship(&conn, 10, 30, 1.0, "ns");
insert_relationship(&conn, 20, 40, 1.0, "ns");
assert_eq!(reached(&conn, &[1, 2], "ns", 0.5, 1), vec![3, 4]);
}
#[test]
fn result_without_duplicates() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
link_memory_entity(&conn, 1, 11);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_relationship(&conn, 10, 20, 1.0, "ns");
insert_relationship(&conn, 11, 20, 1.0, "ns");
let result = reached(&conn, &[1], "ns", 0.5, 1);
assert_eq!(result.len(), 1);
assert_eq!(result, vec![2]);
}
#[test]
fn single_node_without_neighbors_returns_empty() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
assert!(reached(&conn, &[1], "ns", 0.5, 5).is_empty());
}
#[test]
fn cycle_does_not_cause_infinite_loop() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
insert_memory(&conn, 2, "ns", false);
link_memory_entity(&conn, 2, 20);
insert_memory(&conn, 3, "ns", false);
link_memory_entity(&conn, 3, 30);
insert_relationship(&conn, 10, 20, 1.0, "ns");
insert_relationship(&conn, 20, 30, 1.0, "ns");
insert_relationship(&conn, 30, 10, 1.0, "ns");
assert_eq!(reached(&conn, &[1], "ns", 0.5, 10), vec![2, 3]);
}
#[test]
fn neighbor_cap_keeps_strongest_edges() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
for (mem, ent, weight) in [(2i64, 20i64, 0.9f64), (3, 30, 0.8), (4, 40, 0.7)] {
insert_memory(&conn, mem, "ns", false);
link_memory_entity(&conn, mem, ent);
insert_relationship(&conn, 10, ent, weight, "ns");
}
let capped =
traverse_from_memories_with_hops_capped(&conn, &[1], "ns", 0.0, 1, Some(2)).unwrap();
assert_eq!(capped, vec![(2, 1), (3, 1)]);
let uncapped =
traverse_from_memories_with_hops_capped(&conn, &[1], "ns", 0.0, 1, None).unwrap();
assert_eq!(uncapped.len(), 3);
}
#[test]
fn hop_count_is_minimum_distance() {
let conn = setup_db();
insert_memory(&conn, 1, "ns", false);
link_memory_entity(&conn, 1, 10);
for (mem, ent) in [(2i64, 20i64), (3, 30), (4, 40)] {
insert_memory(&conn, mem, "ns", false);
link_memory_entity(&conn, mem, ent);
}
insert_relationship(&conn, 10, 30, 1.0, "ns");
insert_relationship(&conn, 30, 40, 1.0, "ns");
insert_relationship(&conn, 40, 20, 1.0, "ns");
insert_relationship(&conn, 10, 20, 1.0, "ns");
let result = traverse_from_memories_with_hops(&conn, &[1], "ns", 0.5, 3).unwrap();
let hop_of_2 = result.iter().find(|(id, _)| *id == 2).map(|&(_, h)| h);
assert_eq!(hop_of_2, Some(1), "BFS deve reportar a distância mínima");
}
#[test]
fn walk_direction_controls_edge_following() {
let conn = setup_db();
insert_relationship(&conn, 10, 20, 1.0, "ns");
let directed = GraphWalk::directed(0.0, 3)
.run(&SqlNeighbors::new(&conn, "ns"), &[20])
.unwrap();
assert_eq!(directed.depth.len(), 1);
let bidirectional = GraphWalk::bidirectional(0.0, 3)
.run(&SqlNeighbors::new(&conn, "ns"), &[20])
.unwrap();
assert_eq!(bidirectional.depth.get(&10), Some(&1));
}