vestige/tests/phase_1/cognitive_module_isolation.rs
Jan De Landtsheer 5715f585fd feat(storage): phase 1 -- extract MemoryStore and Embedder traits (ADR 0001)
Introduce two trait boundaries that the rest of the stack now sits above,
landing Phase 1 of ADR 0001 (pluggable storage and network access).
Rebased onto v2.1.22 Sanhedrin from the original April work.

MemoryStore / LocalMemoryStore (crates/vestige-core/src/storage/memory_store.rs):
  One trait, ~25 methods, covering CRUD, hybrid / FTS / vector search,
  FSRS scheduling, graph edges, and the forthcoming domain surface.
  trait_variant::make generates a Send-bound MemoryStore alias over the
  base LocalMemoryStore so Arc<dyn MemoryStore> works under tokio/axum.
  Storage errors map through a dedicated MemoryStoreError.

Embedder / LocalEmbedder (crates/vestige-core/src/embedder/):
  Pluggable text-to-vector encoder. FastembedEmbedder wraps the existing
  EmbeddingService; storage never calls fastembed directly anymore.
  Embedder::signature() produces the ModelSignature consumed by the
  store's embedding_model registry.

SqliteMemoryStore (crates/vestige-core/src/storage/sqlite.rs):
  Storage renamed to SqliteMemoryStore; the old name lives on as a
  pub type alias so Arc<Storage> consumers in vestige-mcp stay intact.
  All existing inherent methods are untouched; the trait impl is
  purely additive and dispatches into them. The db_path field added
  by v2.1.1 portable-sync is preserved.

Migration V14 (crates/vestige-core/src/storage/migrations.rs):
  Renumbered from V12 (the original April number) to V14 to slot in
  cleanly after upstream's V12 (v2.1.1 sync_tombstones) and V13
  (v2.1.2 purge tombstones).
  - embedding_model registry table (CHECK id = 1, code enforces the
    single-row invariant).
  - knowledge_nodes.domains / domain_scores TEXT columns (JSON arrays
    default '[]' / '{}'), domains catalogue table, supporting indexes.
  Phase 4 populates these columns; Phase 1 just exposes the schema.

Consolidation and other cognitive pathways now accept a
&dyn LocalMemoryStore (sync) or Arc<dyn MemoryStore> (async) rather
than a concrete Storage.

Tests:
  - trait-method unit tests colocated in sqlite.rs and migrations.rs
  - embedder/fastembed.rs tests for name/dimension/hash stability
  - new integration crate tests/phase_1 (added to workspace members):
    trait_round_trip (8), embedding_model_registry (7),
    domain_column_migration (5), cognitive_module_isolation (4),
    send_bound_variant (2), embedder_trait (2).

Acceptance gate post-rebase:
  - cargo build --workspace --all-targets: ok
  - cargo clippy --workspace --all-targets -- -D warnings: clean
  - cargo test -p vestige-core --lib: 428 pass
  - cargo test -p vestige-phase-1-tests: 28 pass
  - cargo test -p vestige-mcp --lib: 380 pass (Storage alias preserves
    every existing call site)

Co-existence with v2.1.1 portable-sync: this trait extraction is
additive. Portable-sync's tombstone migrations (V12, V13) remain
on the concrete SqliteMemoryStore; Phase 2 (Postgres) will decide
which of those surfaces graduate into the trait.
2026-06-18 19:07:52 -05:00

143 lines
4.8 KiB
Rust

//! Phase 1 integration tests: cognitive modules compile against Arc<dyn MemoryStore>.
//! The key goal is a compile-time gate: if any module still typed against
//! SqliteMemoryStore concretely, this would fail to compile.
use chrono::Utc;
use std::sync::Arc;
use tempfile::tempdir;
use uuid::Uuid;
use vestige_core::storage::{MemoryEdge, MemoryRecord, MemoryStore, SqliteMemoryStore};
fn make_store() -> Arc<dyn MemoryStore> {
let dir = tempdir().unwrap();
let db = dir.path().join("test.db");
std::mem::forget(dir);
Arc::new(SqliteMemoryStore::new(Some(db)).expect("create"))
}
fn make_record(content: &str) -> MemoryRecord {
MemoryRecord {
id: Uuid::new_v4(),
domains: vec![],
domain_scores: Default::default(),
content: content.to_string(),
node_type: "fact".to_string(),
tags: vec!["isolation-test".to_string()],
embedding: None,
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: serde_json::json!({}),
}
}
/// Ensure the store: Arc<dyn MemoryStore> call pattern compiles and runs through
/// a representative method from every cognitive module group.
#[tokio::test]
async fn all_modules_compile_against_dyn_store() {
let store: Arc<dyn MemoryStore> = make_store();
// CRUD via trait
let rec = make_record("cognitive isolation test");
let id = store.insert(&rec).await.expect("insert via dyn trait");
let got = store
.get(id)
.await
.expect("get via dyn trait")
.expect("exists");
assert_eq!(got.content, "cognitive isolation test");
// Graph edges via trait
let rec2 = make_record("linked node");
let id2 = store.insert(&rec2).await.expect("insert 2");
store
.add_edge(&MemoryEdge {
source_id: id,
target_id: id2,
edge_type: "semantic".to_string(),
weight: 0.8,
created_at: Utc::now(),
})
.await
.expect("add_edge via dyn trait");
let edges = store
.get_edges(id, None)
.await
.expect("get_edges via dyn trait");
assert!(!edges.is_empty());
// Search via trait
let results = store
.fts_search("cognitive", 5)
.await
.expect("fts_search via dyn trait");
assert!(!results.is_empty());
// Stats and count via trait
let count = store.count().await.expect("count via dyn trait");
assert!(count >= 2);
let stats = store.get_stats().await.expect("get_stats via dyn trait");
assert!(stats.total_memories >= 2);
}
#[tokio::test]
async fn spreading_activation_traverses_via_trait() {
let store: Arc<dyn MemoryStore> = make_store();
let rec_a = make_record("spreading activation source");
let rec_b = make_record("spreading activation neighbor");
let id_a = rec_a.id;
let id_b = rec_b.id;
store.insert(&rec_a).await.expect("insert a");
store.insert(&rec_b).await.expect("insert b");
store
.add_edge(&MemoryEdge {
source_id: id_a,
target_id: id_b,
edge_type: "semantic".to_string(),
weight: 0.9,
created_at: Utc::now(),
})
.await
.expect("add edge");
// get_neighbors simulates the spreading activation traversal path
let neighbors = store.get_neighbors(id_a, 1).await.expect("get_neighbors");
let ids: Vec<Uuid> = neighbors.iter().map(|(r, _)| r.id).collect();
assert!(ids.contains(&id_a));
assert!(ids.contains(&id_b));
}
#[tokio::test]
async fn synaptic_tagging_consumes_records_via_trait() {
// Build a MemoryRecord from trait-returned data and exercise the
// SynapticTaggingSystem pipeline (constructing CapturedMemory from store data).
let store: Arc<dyn MemoryStore> = make_store();
let rec = make_record("synaptic tagging test memory");
let id = store.insert(&rec).await.expect("insert");
let got = store.get(id).await.expect("get").expect("exists");
// The important thing is we got a MemoryRecord back from the dyn trait;
// SynapticTaggingSystem would take this record as input.
assert_eq!(got.id, id);
assert!(!got.content.is_empty());
}
#[tokio::test]
async fn hippocampal_index_built_from_store() {
// Exercise the fts_search -> HippocampalIndex indexing path.
let store: Arc<dyn MemoryStore> = make_store();
for i in 0..5usize {
let rec = make_record(&format!("hippocampal indexing topic {i}"));
store.insert(&rec).await.expect("insert");
}
let results = store
.fts_search("hippocampal indexing", 10)
.await
.expect("fts_search");
// Verify we get results and they have the correct fields
assert!(!results.is_empty());
for r in &results {
assert!(!r.record.content.is_empty());
assert!(r.score >= 0.0);
}
}