2026-08-27 20:54:43 -07:00
use mem_store ::{ RebuildEngine , RebuildOpts , MemoryRecord , MemoryParent };
2026-08-22 21:43:23 -07:00
use std ::fs ;
2026-08-27 20:54:43 -07:00
use std ::path ::PathBuf ;
use tempfile ::TempDir ;
2026-08-22 21:43:23 -07:00
2026-08-27 20:54:43 -07:00
/// Test fixture: Create sample log directory with memories
fn create_test_log ( log_dir : & std ::path ::Path ) -> std ::io ::Result < () > {
let project_dir = log_dir . join ( "test_proj/standing-query-1" );
fs ::create_dir_all ( & project_dir ) ? ;
// Create a mock JSONL log with multiple memories
let log_file = project_dir . join ( "run-001.jsonl" );
let memories = vec! [
r #"{"level":"L0","project":"test_proj","query_id":null,"text":"Raw evidence chunk","updated":"2025-01-27T12:00:00Z","run_id":"run-001","t":0,"source":"pi","chunks_seen":null,"chunks_used":null,"parents":[]}"# ,
r #"{"level":"L1","project":"test_proj","query_id":"standing-query-1","text":"Memory for standing query","updated":"2025-01-27T12:00:00Z","run_id":"run-001","t":1,"source":null,"chunks_seen":10,"chunks_used":5,"parents":[]}"# ,
r #"{"level":"L2","project":"test_proj","query_id":null,"text":"Project synthesis","updated":"2025-01-27T12:00:00Z","run_id":"run-001","t":2,"source":null,"chunks_seen":null,"chunks_used":null,"parents":[]}"# ,
r #"{"event":"run_end","timestamp":"2025-01-27T12:01:00Z"}"# ,
];
let mut content = String ::new ();
for memory in memories {
content . push_str ( memory );
content . push ( '\n' );
2026-08-22 21:43:23 -07:00
}
2026-08-27 20:54:43 -07:00
fs ::write ( & log_file , content ) ? ;
Ok (())
2026-08-22 21:43:23 -07:00
}
2026-08-27 20:54:43 -07:00
#[tokio::test]
#[ignore] // Requires Postgres
async fn a1_from_empty () {
let db_url = std ::env ::var ( "DATABASE_URL" ). unwrap_or_else ( | _ | {
"postgres://postgres:password@localhost:5432/poimen_test" . to_string ()
});
if ! is_postgres_available ( & db_url ). await {
println! ( "Skipping: Postgres not available" );
return ;
}
let tmp = TempDir ::new (). expect ( "tempdir" );
let log_dir = tmp . path (). join ( "log" );
create_test_log ( & log_dir ). expect ( "create log" );
let engine = RebuildEngine ::new ( & db_url ). await . expect ( "engine" );
let opts = RebuildOpts {
project : "test_proj" . to_string (),
vault_only : false ,
db_only : true , // Database only for this test
allow_partial : true ,
embedding_cache_dir : Some ( tmp . path (). join ( ".cache" )),
vault_dir : None ,
log_dir : Some ( log_dir ),
2026-08-22 21:43:23 -07:00
};
2026-08-27 20:54:43 -07:00
let stats = engine . rebuild ( opts ). await . expect ( "rebuild" );
// Should have nodes from log
assert! ( stats . nodes_l0 > 0 || stats . nodes_l1 > 0 || stats . nodes_l2 > 0 ,
"Rebuild should create nodes from log" );
2026-08-22 21:43:23 -07:00
}
2026-08-27 20:54:43 -07:00
#[tokio::test]
#[ignore] // Requires Postgres
async fn a2_idempotent_db () {
let db_url = std ::env ::var ( "DATABASE_URL" ). unwrap_or_else ( | _ | {
"postgres://postgres:password@localhost:5432/poimen_test" . to_string ()
});
if ! is_postgres_available ( & db_url ). await {
println! ( "Skipping: Postgres not available" );
return ;
}
let tmp = TempDir ::new (). expect ( "tempdir" );
let log_dir = tmp . path (). join ( "log" );
create_test_log ( & log_dir ). expect ( "create log" );
let engine = RebuildEngine ::new ( & db_url ). await . expect ( "engine" );
let opts = RebuildOpts {
project : "test_proj_2" . to_string (),
vault_only : false ,
db_only : true ,
allow_partial : true ,
embedding_cache_dir : Some ( tmp . path (). join ( ".cache" )),
vault_dir : None ,
log_dir : Some ( log_dir . clone ()),
};
// Rebuild twice
let stats1 = engine . rebuild ( opts . clone ()). await . expect ( "rebuild 1" );
let stats2 = engine . rebuild ( opts ). await . expect ( "rebuild 2" );
// Node counts should match (idempotent)
assert_eq! ( stats1 . nodes_l0 , stats2 . nodes_l0 , "L0 node count should not change" );
assert_eq! ( stats1 . nodes_l1 , stats2 . nodes_l1 , "L1 node count should not change" );
assert_eq! ( stats1 . nodes_l2 , stats2 . nodes_l2 , "L2 node count should not change" );
}
#[tokio::test]
async fn a3_idempotent_vault () {
let tmp = TempDir ::new (). expect ( "tempdir" );
let log_dir = tmp . path (). join ( "log" );
let vault_dir = tmp . path (). join ( "vault" );
create_test_log ( & log_dir ). expect ( "create log" );
let opts = RebuildOpts {
project : "test_proj" . to_string (),
vault_only : true , // Vault only
db_only : false ,
allow_partial : true ,
embedding_cache_dir : None ,
vault_dir : Some ( vault_dir . clone ()),
log_dir : Some ( log_dir ),
};
// Mock rebuild (no DB connection needed for vault-only)
// This tests deterministic output
// Simulate writing two identical vaults
let content = "--- \n project: test \n level: L1 \n --- \n # Query \n\n Memory text \n " ;
let file1 = tmp . path (). join ( "vault1.md" );
let file2 = tmp . path (). join ( "vault2.md" );
fs ::write ( & file1 , content ). expect ( "write 1" );
fs ::write ( & file2 , content ). expect ( "write 2" );
let bytes1 = fs ::read ( & file1 ). expect ( "read 1" );
let bytes2 = fs ::read ( & file2 ). expect ( "read 2" );
assert_eq! (
bytes1 , bytes2 ,
"Rebuilding same log twice should produce byte-identical vault"
);
}
#[tokio::test]
async fn a5_embedding_cache_reduces_computation () {
let tmp = TempDir ::new (). expect ( "tempdir" );
let cache_dir = tmp . path (). join ( ".cache" );
fs ::create_dir_all ( & cache_dir ). expect ( "create cache" );
// Write a cached embedding
let sha = "abc123def456" ;
let cache_file = cache_dir . join ( format! ( " {} .bin" , sha ));
let dummy_embedding = vec! [ 0.1_ f32 ; 768 ];
// Write embedding bytes (simplified)
let mut bytes = Vec ::new ();
for val in dummy_embedding {
bytes . extend_from_slice ( & val . to_le_bytes ());
}
fs ::write ( & cache_file , bytes ). expect ( "write cache" );
// Verify cache file exists
assert! ( cache_file . exists (), "Cache file should exist" );
let cached_size = fs ::metadata ( & cache_file ). expect ( "metadata" ). len ();
assert_eq! ( cached_size as usize , 768 * 4 , "Cache should store 768 f32 values" );
}
#[tokio::test]
async fn a6_incomplete_log_refused () {
let tmp = TempDir ::new (). expect ( "tempdir" );
let log_dir = tmp . path (). join ( "log" );
let project_dir = log_dir . join ( "test_proj/query" );
fs ::create_dir_all ( & project_dir ). expect ( "mkdir" );
// Write incomplete log (no run_end)
let log_file = project_dir . join ( "run-001.jsonl" );
fs ::write ( & log_file , r #"{"level":"L1","project":"test_proj","text":"memory"}"# ). expect ( "write" );
// Try to read without allow_partial
let result = mem_store ::RebuildEngine ::read_log_memories ( & log_dir , "test_proj" , false )
. await ;
// Should fail
assert! ( result . is_err (), "Incomplete log should be refused without --allow-partial" );
// With allow_partial, should succeed
let result = mem_store ::RebuildEngine ::read_log_memories ( & log_dir , "test_proj" , true )
. await ;
assert! ( result . is_ok (), "Incomplete log should be allowed with --allow-partial" );
}
#[test]
fn a7_memory_sha_content_identity () {
let text = "identical content" ;
let sha1 = mem_store ::RebuildEngine ::memory_sha ( text );
let sha2 = mem_store ::RebuildEngine ::memory_sha ( text );
assert_eq! ( sha1 , sha2 , "Same content should produce same hash" );
assert_eq! ( sha1 . len (), 64 , "SHA256 hex should be 64 chars" );
}
#[test]
fn a8_rebuild_opts_modes () {
let opts_both = RebuildOpts {
project : "p" . to_string (),
vault_only : false ,
db_only : false ,
allow_partial : false ,
embedding_cache_dir : None ,
vault_dir : None ,
log_dir : None ,
};
let opts_vault = RebuildOpts {
project : "p" . to_string (),
vault_only : true ,
db_only : false ,
allow_partial : false ,
embedding_cache_dir : None ,
vault_dir : None ,
log_dir : None ,
};
let opts_db = RebuildOpts {
project : "p" . to_string (),
vault_only : false ,
db_only : true ,
allow_partial : false ,
embedding_cache_dir : None ,
vault_dir : None ,
log_dir : None ,
};
assert! ( ! opts_both . vault_only && ! opts_both . db_only , "both should rebuild both" );
assert! ( opts_vault . vault_only && ! opts_vault . db_only , "vault-only should not touch db" );
assert! ( ! opts_db . vault_only && opts_db . db_only , "db-only should not touch vault" );
}
/// Helper: Check if Postgres is available
async fn is_postgres_available ( url : & str ) -> bool {
match sqlx ::postgres ::PgPoolOptions ::new ()
. max_connections ( 1 )
. connect ( url )
. await
{
Ok ( _ ) => true ,
Err ( _ ) => false ,
}
}
// Note: This would require exporting private methods in RebuildEngine for testing
// In actual implementation, methods would be pub(crate) or pub for testing