use super::*;
use chrono::{TimeZone, Utc};
use tempfile::TempDir;
use crate::memory::chunks::{chunk_id, upsert_chunks, Chunk, Metadata, SourceKind, SourceRef};
use crate::memory::config::MemoryConfig;
use crate::memory::tree::bucket_seal::LeafRef;
use crate::memory::tree::store::{self, TreeStatus};
use crate::memory::tree::summarise::ConcatSummariser;
fn test_config() -> (TempDir, MemoryConfig) {
let tmp = TempDir::new().unwrap();
let cfg = MemoryConfig::new(tmp.path());
(tmp, cfg)
}
fn seed_chunk(cfg: &MemoryConfig, seq: u32, content: &str) -> Chunk {
let ts = Utc
.timestamp_millis_opt(1_700_000_000_000 + seq as i64)
.unwrap();
let chunk = Chunk {
id: chunk_id(SourceKind::Chat, "slack:#eng", seq, content),
content: content.to_string(),
metadata: Metadata {
source_kind: SourceKind::Chat,
source_id: "slack:#eng".into(),
owner: "alice".into(),
timestamp: ts,
time_range: (ts, ts),
tags: vec!["eng".into()],
source_ref: Some(SourceRef::new("slack://eng/thread")),
path_scope: None,
},
token_count: 80,
seq_in_source: seq,
created_at: ts,
partial_message: false,
};
upsert_chunks(cfg, &[chunk.clone()]).unwrap();
chunk
}
#[test]
fn source_factory_uses_source_kind_and_full_scope() {
let f = TreeFactory::source("slack:#eng");
assert_eq!(f.kind(), TreeKind::Source);
assert_eq!(f.scope(), "slack:#eng");
assert_eq!(f.profile(), TreeProfile::Source);
}
#[test]
fn global_uses_global_scope_and_kind() {
let g = TreeFactory::global();
assert_eq!(g.kind(), TreeKind::Global);
assert_eq!(g.scope(), GLOBAL_SCOPE);
}
#[test]
fn source_label_strategy_extracts_topic_empty() {
assert!(matches!(
TreeFactory::source("slack:#eng").label_strategy(),
LabelStrategy::ExtractFromContent(_)
));
assert!(matches!(
TreeFactory::topic("email:alice@example.com").label_strategy(),
LabelStrategy::Empty
));
}
#[test]
fn from_tree_round_trips_kind() {
let tree = Tree {
id: "t".into(),
kind: TreeKind::Topic,
scope: "person:alice".into(),
root_id: None,
max_level: 0,
status: crate::memory::tree::store::TreeStatus::Active,
created_at: chrono::Utc::now(),
last_sealed_at: None,
};
let f = TreeFactory::from_tree(&tree);
assert_eq!(f.kind(), TreeKind::Topic);
assert_eq!(f.scope(), "person:alice");
}
#[test]
fn get_or_create_persists_tree_for_each_profile() {
let (_tmp, cfg) = test_config();
let source = TreeFactory::source("slack:#eng")
.get_or_create(&cfg)
.unwrap();
let topic = TreeFactory::topic("person:alice")
.get_or_create(&cfg)
.unwrap();
let global = TreeFactory::global().get_or_create(&cfg).unwrap();
assert_eq!(source.kind, TreeKind::Source);
assert_eq!(source.scope, "slack:#eng");
assert_eq!(topic.kind, TreeKind::Topic);
assert_eq!(topic.scope, "person:alice");
assert_eq!(global.kind, TreeKind::Global);
assert_eq!(global.scope, GLOBAL_SCOPE);
}
#[tokio::test]
async fn factory_insert_seal_and_archive_use_profile_scope() {
let (_tmp, cfg) = test_config();
let factory = TreeFactory::source("slack:#eng");
let summariser = ConcatSummariser::new();
let chunk = seed_chunk(&cfg, 0, "Alice is coordinating the launch plan");
let leaf = LeafRef {
chunk_id: chunk.id.clone(),
token_count: cfg.tree.input_token_budget,
timestamp: chunk.created_at,
content: chunk.content.clone(),
entities: vec!["person:alice".into()],
topics: vec!["topic:launch".into()],
score: 0.9,
};
let immediate = factory.insert_leaf(&cfg, &leaf, &summariser).await.unwrap();
assert_eq!(immediate.len(), 1);
let no_more_work = factory.seal_now(&cfg, &summariser).await.unwrap();
assert!(no_more_work.is_empty());
let tree = factory.get_or_create(&cfg).unwrap();
assert_eq!(tree.root_id.as_deref(), Some(immediate[0].as_str()));
assert_eq!(store::count_summaries(&cfg, &tree.id).unwrap(), 1);
factory.archive(&cfg).unwrap();
let archived = store::get_tree(&cfg, &tree.id).unwrap().unwrap();
assert_eq!(archived.status, TreeStatus::Archived);
}