use chrono::{DateTime, TimeZone, Utc};
use tempfile::TempDir;
use crate::memory::chunks::{
chunk_id, tree_active_signature, upsert_chunks, with_connection, Chunk, Metadata, SourceKind,
SourceRef,
};
use crate::memory::config::MemoryConfig;
use crate::memory::score::extract::EntityKind;
use crate::memory::score::resolver::CanonicalEntity;
use crate::memory::score::signals::ScoreSignals;
use crate::memory::score::store::{index_entities, upsert_score, ScoreRow};
use crate::memory::tree::store::{
insert_summary_tx, insert_tree, set_summary_embedding_for_signature,
};
use crate::memory::tree::{SummaryNode, Tree, TreeKind, TreeStatus};
pub fn test_config() -> (TempDir, MemoryConfig) {
let tmp = TempDir::new().unwrap();
let cfg = MemoryConfig::new(tmp.path());
(tmp, cfg)
}
pub fn fixed_ts() -> DateTime<Utc> {
Utc.timestamp_millis_opt(1_700_000_000_000).unwrap()
}
pub fn sample_chunk(source: &str, seq: u32, content: &str) -> Chunk {
sample_chunk_at(source, seq, content, fixed_ts())
}
pub fn sample_chunk_at(source: &str, seq: u32, content: &str, ts: DateTime<Utc>) -> Chunk {
Chunk {
id: chunk_id(SourceKind::Chat, source, seq, content),
content: content.to_string(),
metadata: Metadata {
source_kind: SourceKind::Chat,
source_id: source.into(),
owner: "alice".into(),
timestamp: ts,
time_range: (ts, ts),
tags: vec![],
source_ref: Some(SourceRef::new(format!("slack://{source}/{seq}"))),
path_scope: None,
},
token_count: 20,
seq_in_source: seq,
created_at: ts,
partial_message: false,
}
}
pub fn insert_chunks(cfg: &MemoryConfig, chunks: &[Chunk]) {
upsert_chunks(cfg, chunks).unwrap();
}
pub fn insert_score(cfg: &MemoryConfig, chunk_id: &str, total: f32) {
upsert_score(
cfg,
&ScoreRow {
chunk_id: chunk_id.to_string(),
total,
signals: ScoreSignals::default(),
dropped: false,
reason: None,
computed_at_ms: 0,
llm_importance_reason: None,
},
)
.unwrap();
}
pub fn insert_tree_row(cfg: &MemoryConfig, tree: &Tree) {
insert_tree(cfg, tree).unwrap();
}
pub fn insert_summary(cfg: &MemoryConfig, node: &SummaryNode) {
let sig = tree_active_signature(cfg);
with_connection(cfg, |conn| {
let tx = conn.unchecked_transaction()?;
insert_summary_tx(&tx, node, &sig)?;
tx.commit()?;
Ok(())
})
.unwrap();
}
pub fn set_summary_embedding(cfg: &MemoryConfig, summary_id: &str, vec: &[f32]) {
let sig = tree_active_signature(cfg);
set_summary_embedding_for_signature(cfg, summary_id, &sig, vec).unwrap();
}
pub fn index_entity_occurrence(
cfg: &MemoryConfig,
canonical_id: &str,
kind: EntityKind,
surface: &str,
node_id: &str,
node_kind: &str,
timestamp_ms: i64,
tree_id: Option<&str>,
) {
let entity = CanonicalEntity {
canonical_id: canonical_id.to_string(),
kind,
surface: surface.to_string(),
span_start: 0,
span_end: 0,
score: 1.0,
};
index_entities(cfg, &[entity], node_id, node_kind, timestamp_ms, tree_id).unwrap();
}
pub fn source_tree(id: &str, scope: &str, root_id: Option<&str>, max_level: u32) -> Tree {
let ts = fixed_ts();
Tree {
id: id.into(),
kind: TreeKind::Source,
scope: scope.into(),
root_id: root_id.map(|s| s.to_string()),
max_level,
status: TreeStatus::Active,
created_at: ts,
last_sealed_at: Some(ts),
}
}
#[allow(clippy::too_many_arguments)]
pub fn summary_node(
id: &str,
tree_id: &str,
level: u32,
parent_id: Option<&str>,
child_ids: &[&str],
content: &str,
ts: DateTime<Utc>,
) -> SummaryNode {
SummaryNode {
id: id.into(),
tree_id: tree_id.into(),
tree_kind: TreeKind::Source,
level,
parent_id: parent_id.map(|s| s.to_string()),
child_ids: child_ids.iter().map(|s| s.to_string()).collect(),
content: content.into(),
token_count: 10,
entities: vec![],
topics: vec![],
time_range_start: ts,
time_range_end: ts,
score: 0.5,
sealed_at: ts,
deleted: false,
embedding: None,
doc_id: None,
version_ms: None,
}
}