use super::concurrency::{
DEFAULT_DREAM_MAX_CONCURRENT, DreamCycleGauge, acquire_dream_permit, dream_cycles_in_flight,
dream_cycles_peak_in_flight, dream_max_concurrent, parse_max_concurrent, stagger_offset,
};
use super::config::DreamConfig;
use super::cycle::{DREAM_EMBED_CHUNK, dedup_pass_with_embedder};
use super::dreamer::Dreamer;
use super::semantic::consolidate_scoped_within;
use crate::embedder::MockEmbedder;
use crate::memory_core::embed::{EMBED_DIM, Embedder};
use crate::memory_core::palace::{Drawer, Palace, PalaceId, RoomType};
use crate::memory_core::retrieval::{PalaceHandle, seed_shared_embedder_with_mock};
use crate::memory_core::semantic_consolidation::SemanticConsolidationConfig;
use anyhow::Result;
use async_trait::async_trait;
use chrono::Utc;
use serial_test::serial;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tempfile::tempdir;
use uuid::Uuid;
async fn open_handle(name: &str) -> Arc<PalaceHandle> {
seed_shared_embedder_with_mock();
let dir = tempdir().expect("tempdir");
let palace = Palace {
id: PalaceId::new(name),
name: name.into(),
description: None,
created_at: Utc::now(),
data_dir: dir.path().join(name),
};
std::fs::create_dir_all(&palace.data_dir).expect("palace data dir");
let handle = PalaceHandle::open(&palace).expect("open palace");
std::mem::forget(dir);
handle
}
fn quiet_config() -> DreamConfig {
DreamConfig {
semantic: SemanticConsolidationConfig {
enabled: false,
..Default::default()
},
recall_benchmark_enabled: false,
compact: false,
..DreamConfig::default()
}
}
fn seed_drawers(handle: &Arc<PalaceHandle>, count: usize) {
let room_id = Uuid::new_v4();
let mut drawers = handle.drawers.write();
for i in 0..count {
drawers.push(Drawer::new(
room_id,
format!("drawer number {i} of {count}"),
));
}
}
struct RecordingEmbedder {
inner: MockEmbedder,
batches: Arc<Mutex<Vec<usize>>>,
per_batch_delay: Duration,
}
impl RecordingEmbedder {
fn new(per_batch_delay: Duration) -> Self {
Self {
inner: MockEmbedder::new(EMBED_DIM),
batches: Arc::new(Mutex::new(Vec::new())),
per_batch_delay,
}
}
fn recorded(&self) -> Vec<usize> {
self.batches.lock().expect("batch log not poisoned").clone()
}
}
#[async_trait]
impl Embedder for RecordingEmbedder {
async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
self.batches
.lock()
.expect("batch log not poisoned")
.push(texts.len());
if !self.per_batch_delay.is_zero() {
tokio::time::sleep(self.per_batch_delay).await;
}
self.inner.embed_batch(texts).await
}
fn dimension(&self) -> usize {
self.inner.dimension()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn dream_permits_cap_concurrent_holders() {
let cap = dream_max_concurrent();
let live = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let mut joins = Vec::new();
for _ in 0..20 {
let live = Arc::clone(&live);
let peak = Arc::clone(&peak);
joins.push(tokio::spawn(async move {
let _permit = acquire_dream_permit().await;
let now = live.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(5)).await;
live.fetch_sub(1, Ordering::SeqCst);
}));
}
for join in joins {
join.await.expect("permit task");
}
assert!(
peak.load(Ordering::SeqCst) <= cap,
"at most {cap} permit holders at once; observed {}",
peak.load(Ordering::SeqCst)
);
assert_eq!(
live.load(Ordering::SeqCst),
0,
"every permit released; a leak would leave holders counted"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ten_palaces_never_exceed_the_concurrency_cap() {
const PALACES: usize = 10;
let cap = dream_max_concurrent();
let mut handles = Vec::new();
for i in 0..PALACES {
let handle = open_handle(&format!("herd-palace-{i}")).await;
seed_drawers(&handle, 300);
handles.push(handle);
}
let barrier = Arc::new(tokio::sync::Barrier::new(PALACES));
let mut joins = Vec::new();
for handle in handles {
let barrier = Arc::clone(&barrier);
joins.push(tokio::spawn(async move {
let dreamer = Dreamer::new(quiet_config());
barrier.wait().await;
dreamer.dream_cycle(&handle).await.expect("dream cycle");
}));
}
for join in joins {
join.await.expect("cycle task");
}
let peak = dream_cycles_peak_in_flight();
assert!(
peak <= cap,
"{PALACES} palaces released together must never run more than {cap} cycles at once; \
peak in flight was {peak}"
);
assert!(peak >= 1, "the in-flight gauge never moved: {peak}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial(dream_permits)]
async fn a_dream_cycle_waits_when_every_permit_is_held() {
let handle = open_handle("permit-wait").await;
let cap = dream_max_concurrent();
let mut held = Vec::new();
for _ in 0..cap {
held.push(acquire_dream_permit().await);
}
let done = Arc::new(AtomicUsize::new(0));
let done_task = Arc::clone(&done);
let join = tokio::spawn(async move {
let dreamer = Dreamer::new(quiet_config());
dreamer.dream_cycle(&handle).await.expect("dream cycle");
done_task.store(1, Ordering::SeqCst);
});
tokio::time::sleep(Duration::from_millis(300)).await;
assert_eq!(
done.load(Ordering::SeqCst),
0,
"a dream cycle must queue behind the concurrency cap, not run through it"
);
drop(held);
tokio::time::timeout(Duration::from_secs(60), join)
.await
.expect("cycle finished once a permit freed up")
.expect("cycle task");
assert_eq!(done.load(Ordering::SeqCst), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial(dream_permits)]
async fn an_interactive_dream_errors_when_the_dreamer_is_busy() {
let handle = open_handle("interactive-busy").await;
let cap = dream_max_concurrent();
let mut held = Vec::new();
for _ in 0..cap {
held.push(acquire_dream_permit().await);
}
let started = Instant::now();
let outcome = tokio::time::timeout(
Duration::from_secs(10),
consolidate_scoped_within(
&handle,
&quiet_config(),
None,
7,
None,
Duration::from_millis(500),
),
)
.await
.expect("an interactive dream must not wait indefinitely for a permit");
let elapsed = started.elapsed();
let err = outcome.expect_err("a busy dreamer must be an error, not a silent no-op");
let text = format!("{err:#}");
assert!(
text.contains("dreamer busy"),
"the error must name the condition the caller can act on; got: {text}"
);
assert!(
elapsed < Duration::from_secs(5),
"the wait must be bounded by the caller's ceiling; took {elapsed:?}"
);
drop(held);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_wedged_embedder_ends_the_dedup_pass_with_an_error() {
let handle = open_handle("wedged-embedder").await;
seed_drawers(&handle, 4);
let embedder = RecordingEmbedder::new(Duration::from_secs(3));
let started = Instant::now();
let err = dedup_pass_with_embedder(
&handle,
Instant::now(),
Duration::from_secs(60),
0.95,
&embedder,
Duration::from_millis(200),
)
.await
.expect_err("a chunk that outlives the embed ceiling must fail the pass");
let text = format!("{err:#}");
assert!(
text.contains("timed out"),
"the error must name the timeout; got: {text}"
);
assert!(
started.elapsed() < Duration::from_secs(2),
"the pass must give up at the ceiling, not wait the embedder out: {:?}",
started.elapsed()
);
}
#[test]
fn parse_max_concurrent_rejects_junk_and_zero() {
assert_eq!(parse_max_concurrent(Some("4")), 4);
assert_eq!(parse_max_concurrent(Some(" 8 ")), 8);
assert_eq!(parse_max_concurrent(None), DEFAULT_DREAM_MAX_CONCURRENT);
assert_eq!(parse_max_concurrent(Some("")), DEFAULT_DREAM_MAX_CONCURRENT);
assert_eq!(
parse_max_concurrent(Some("0")),
DEFAULT_DREAM_MAX_CONCURRENT
);
assert_eq!(
parse_max_concurrent(Some("lots")),
DEFAULT_DREAM_MAX_CONCURRENT
);
assert_eq!(
parse_max_concurrent(Some("-3")),
DEFAULT_DREAM_MAX_CONCURRENT
);
}
#[test]
fn the_default_cap_is_two() {
assert_eq!(DEFAULT_DREAM_MAX_CONCURRENT, 2);
}
#[test]
fn the_in_flight_gauge_counts_a_held_cycle() {
let held = DreamCycleGauge::enter();
let during = dream_cycles_in_flight();
assert!(during >= 1, "a held gauge counts at least itself: {during}");
assert!(
dream_cycles_peak_in_flight() >= during,
"the peak is never below a count it has already seen"
);
drop(held);
}
#[test]
fn stagger_offsets_spread_first_ticks_across_the_interval() {
let interval = Duration::from_secs(300);
let total = 10;
let offsets: Vec<Duration> = (0..total)
.map(|k| stagger_offset(k, total, interval))
.collect();
assert_eq!(
offsets[0],
Duration::ZERO,
"the first palace is not delayed"
);
for (k, offset) in offsets.iter().enumerate() {
assert_eq!(
*offset,
Duration::from_secs(30 * k as u64),
"palace {k} of {total} waits interval * k / n"
);
assert!(
*offset < interval,
"an offset never reaches a full interval"
);
}
for pair in offsets.windows(2) {
assert!(pair[0] < pair[1], "offsets strictly increase: {pair:?}");
}
assert_eq!(stagger_offset(0, 1, interval), Duration::ZERO);
assert_eq!(stagger_offset(0, 0, interval), Duration::ZERO);
assert_eq!(stagger_offset(9, 3, interval), Duration::ZERO);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_dream_loop_waits_its_stagger_before_the_first_cycle() {
let handle = open_handle("stagger-loop").await;
let data_dir = handle
.data_dir
.clone()
.expect("palace handle has a data dir");
let id = handle.id.clone();
let registry = crate::memory_core::PalaceRegistry::new();
registry.register_arc(handle);
let dreamer = Arc::new(Dreamer::new(DreamConfig {
idle_secs: 1,
..quiet_config()
}));
let (tx, rx) = tokio::sync::watch::channel(false);
let join = dreamer.start_with_shutdown(registry, id, Duration::from_secs(3), rx);
tokio::time::sleep(Duration::from_millis(1_600)).await;
let stats = super::config::PersistedDreamStats::load(&data_dir).expect("load stats");
assert!(
stats.is_none(),
"a staggered loop must not have run its first cycle 1.6s in \
(un-staggered it fires at 1.0s)"
);
let _ = tx.send(true);
let _ = tokio::time::timeout(Duration::from_secs(2), join).await;
}
#[tokio::test]
async fn dedup_embeds_in_bounded_chunks() {
let handle = open_handle("chunked-dedup").await;
seed_drawers(&handle, 600);
let embedder = RecordingEmbedder::new(Duration::ZERO);
dedup_pass_with_embedder(
&handle,
Instant::now(),
Duration::from_secs(60),
0.95,
&embedder,
Duration::from_secs(60),
)
.await
.expect("dedup pass");
let batches = embedder.recorded();
let mut expected = vec![DREAM_EMBED_CHUNK; 600 / DREAM_EMBED_CHUNK];
if 600 % DREAM_EMBED_CHUNK > 0 {
expected.push(600 % DREAM_EMBED_CHUNK);
}
assert_eq!(
batches, expected,
"600 drawers must embed as bounded chunks, not one whole-corpus call"
);
assert!(
batches.iter().all(|n| *n <= DREAM_EMBED_CHUNK),
"no batch may exceed the chunk size: {batches:?}"
);
assert_eq!(
batches.iter().sum::<usize>(),
600,
"chunking must not drop or duplicate a drawer"
);
}
#[tokio::test]
async fn the_cycle_budget_stops_dedup_between_chunks() {
let handle = open_handle("budget-dedup").await;
seed_drawers(&handle, 600);
let embedder = RecordingEmbedder::new(Duration::from_millis(200));
dedup_pass_with_embedder(
&handle,
Instant::now(),
Duration::from_millis(50),
0.95,
&embedder,
Duration::from_secs(60),
)
.await
.expect("dedup pass");
assert_eq!(
embedder.recorded(),
vec![DREAM_EMBED_CHUNK],
"the budget must stop the pass between chunks, after exactly one \
chunk-sized embed"
);
}
#[tokio::test]
async fn chunking_preserves_dedup_behaviour() {
let handle = open_handle("chunk-behaviour").await;
for importance in [0.7_f32, 0.6] {
handle
.remember(
"Rust uses HNSW for vector search".into(),
RoomType::Backend,
vec!["rust".into()],
importance,
)
.await
.expect("remember");
}
assert_eq!(handle.drawers.read().len(), 2);
let embedder = RecordingEmbedder::new(Duration::ZERO);
let merged = dedup_pass_with_embedder(
&handle,
Instant::now(),
Duration::from_secs(60),
0.95,
&embedder,
Duration::from_secs(60),
)
.await
.expect("dedup pass");
assert_eq!(merged, 1, "two identical drawers still collapse to one");
assert_eq!(handle.drawers.read().len(), 1);
assert_eq!(embedder.recorded(), vec![2], "one chunk holds both drawers");
}