Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions crates/cortexadb-core/src/core/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,30 @@ impl StateMachine {
Ok(neighbors)
}

// TODO: Get all connected memories transitively via BFS
// pub fn get_connected_transitive(&self, id: MemoryId) -> Result<Vec<MemoryId>>

// TODO: Check if path exists between two nodes
// pub fn has_path(&self, from: MemoryId, to: MemoryId) -> bool

// TODO: Delete all memories in a collection
// pub fn delete_collection(&mut self, collection: &str) -> Result<()>

// TODO: Get all unique collection names
// pub fn collections(&self) -> Vec<String>

// TODO: Get count of memories in a collection
// pub fn collection_len(&self, collection: &str) -> usize

// TODO: Get N most recent memories
// pub fn get_latest(&self, n: usize) -> Vec<&MemoryEntry>

// TODO: Get N oldest memories
// pub fn get_oldest(&self, n: usize) -> Vec<&MemoryEntry>

// TODO: Add multiple entries in bulk
// pub fn add_many(&mut self, entries: Vec<MemoryEntry>) -> Result<()>
Comment on lines +195 to +213
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These commented-out function “signatures” inside the impl can easily drift out of sync and aren’t valid Rust declarations if someone tries to “uncomment” them later (impl fns require bodies). Consider moving this backlog to an issue/tracking doc, or add real stub methods with a minimal body (e.g., returning an error / todo!) behind an explicit feature flag so the compiler keeps signatures honest.

Suggested change
// pub fn has_path(&self, from: MemoryId, to: MemoryId) -> bool
// TODO: Delete all memories in a collection
// pub fn delete_collection(&mut self, collection: &str) -> Result<()>
// TODO: Get all unique collection names
// pub fn collections(&self) -> Vec<String>
// TODO: Get count of memories in a collection
// pub fn collection_len(&self, collection: &str) -> usize
// TODO: Get N most recent memories
// pub fn get_latest(&self, n: usize) -> Vec<&MemoryEntry>
// TODO: Get N oldest memories
// pub fn get_oldest(&self, n: usize) -> Vec<&MemoryEntry>
// TODO: Add multiple entries in bulk
// pub fn add_many(&mut self, entries: Vec<MemoryEntry>) -> Result<()>
// TODO: Implement path existence checks between two memories.
// TODO: Implement deleting all memories in a collection.
// TODO: Implement returning all unique collection names.
// TODO: Implement counting memories in a collection.
// TODO: Implement returning the N most recent memories.
// TODO: Implement returning the N oldest memories.
// TODO: Implement bulk insertion of multiple entries.

Copilot uses AI. Check for mistakes.

/// Get size of state
pub fn len(&self) -> usize {
self.memories.len()
Expand Down Expand Up @@ -386,4 +410,149 @@ mod tests {
let result = sm.connect(MemoryId(1), MemoryId(2), "bad".to_string());
assert!(matches!(result, Err(StateMachineError::CrossCollectionEdge { .. })));
}

#[test]
fn test_disconnect_nonexistent_edge() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(2, "default", 1000)).unwrap();

sm.disconnect(MemoryId(1), MemoryId(2)).unwrap();
sm.disconnect(MemoryId(1), MemoryId(2)).unwrap();

let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert!(neighbors.is_empty());
}

#[test]
fn test_disconnect_from_nonexistent_memory() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();

sm.disconnect(MemoryId(999), MemoryId(1)).unwrap();
assert_eq!(sm.len(), 1);
Comment on lines +428 to +433
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test encodes that disconnect is a no-op even when from does not exist. That’s inconsistent with connect/get_neighbors, which return MemoryNotFound for unknown IDs. Please decide the intended API semantics and align accordingly (either make disconnect return MemoryNotFound for missing from/to and update this test, or rename/expand the test and add documentation explicitly stating disconnect is idempotent even for missing nodes).

Suggested change
fn test_disconnect_from_nonexistent_memory() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.disconnect(MemoryId(999), MemoryId(1)).unwrap();
assert_eq!(sm.len(), 1);
fn test_disconnect_is_idempotent_even_when_endpoint_memories_are_missing() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
// `disconnect` is intentionally a no-op when either endpoint is missing:
// the postcondition is simply that the edge does not exist.
sm.disconnect(MemoryId(999), MemoryId(1)).unwrap();
sm.disconnect(MemoryId(1), MemoryId(999)).unwrap();
assert_eq!(sm.len(), 1);
let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert!(neighbors.is_empty());

Copilot uses AI. Check for mistakes.
}

#[test]
fn test_get_memories_empty_collection() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "ns1", 1000)).unwrap();

let ns2_entries = sm.get_memories_in_collection("ns2");
assert!(ns2_entries.is_empty());

let default_entries = sm.get_memories_in_collection("default");
assert!(default_entries.is_empty());
}

#[test]
fn test_update_with_same_timestamp() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(1, "updated_collection", 1000)).unwrap();

assert_eq!(sm.len(), 1);
let entry = sm.get_memory(MemoryId(1)).unwrap();
assert_eq!(entry.collection, "updated_collection");

let range = sm.get_memories_in_time_range(1000, 1000);
assert_eq!(range.len(), 1);
}

#[test]
fn test_delete_nonexistent_memory() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();

let result = sm.delete(MemoryId(999));
assert!(matches!(result, Err(StateMachineError::MemoryNotFound(..))));
assert_eq!(sm.len(), 1);
}

#[test]
fn test_delete_from_empty_state_machine() {
let mut sm = StateMachine::new();
let result = sm.delete(MemoryId(1));
assert!(matches!(result, Err(StateMachineError::MemoryNotFound(..))));
}

#[test]
fn test_connect_self_referential_edge() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();

let result = sm.connect(MemoryId(1), MemoryId(1), "self_refs".to_string());
assert!(result.is_ok());

let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].0, MemoryId(1));
}

#[test]
fn test_multiple_edges_same_relation_rejected() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(2, "default", 1000)).unwrap();

sm.connect(MemoryId(1), MemoryId(2), "rel".to_string()).unwrap();
sm.connect(MemoryId(1), MemoryId(2), "rel".to_string()).unwrap();

let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert_eq!(neighbors.len(), 1);
}
Comment on lines +492 to +503
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test name is misleading: the second connect call is expected to succeed (it’s unwrapped) and the behavior under test is edge de-duplication, not rejection via an error. Rename the test (and/or assert the exact behavior you want, e.g., that the second call returns Ok and does not increase neighbor count).

Copilot uses AI. Check for mistakes.

#[test]
fn test_different_relations_allowed() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(2, "default", 1000)).unwrap();

sm.connect(MemoryId(1), MemoryId(2), "rel1".to_string()).unwrap();
sm.connect(MemoryId(1), MemoryId(2), "rel2".to_string()).unwrap();

let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert_eq!(neighbors.len(), 2);
}

#[test]
fn test_time_range_no_results() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(2, "default", 2000)).unwrap();

let range = sm.get_memories_in_time_range(500, 800);
assert!(range.is_empty());

let range = sm.get_memories_in_time_range(2500, 3000);
assert!(range.is_empty());
}

#[test]
fn test_get_memory_nonexistent() {
let sm = StateMachine::new();
let result = sm.get_memory(MemoryId(1));
assert!(matches!(result, Err(StateMachineError::MemoryNotFound(..))));
}

#[test]
fn test_neighbors_nonexistent_memory() {
let sm = StateMachine::new();
let result = sm.get_neighbors(MemoryId(1));
assert!(matches!(result, Err(StateMachineError::MemoryNotFound(..))));
}

#[test]
fn test_update_preserves_edges() {
let mut sm = StateMachine::new();
sm.add(create_test_entry(1, "default", 1000)).unwrap();
sm.add(create_test_entry(2, "default", 1000)).unwrap();
sm.connect(MemoryId(1), MemoryId(2), "rel".to_string()).unwrap();

sm.add(create_test_entry(1, "default", 2000)).unwrap();

let neighbors = sm.get_neighbors(MemoryId(1)).unwrap();
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].0, MemoryId(2));
}
}
Loading