use super::*;
use crate::core::sm::config::{SmInferenceConfig, SmRoundsConfig};
use crate::core::sm::context::compaction::COMPRESS_SUMMARY_SYSTEM_PROMPT;
use crate::core::sm::context::mock_provider::MockProvider;
use crate::core::sm::context::model::ToolTrace;
use chrono::{DateTime, TimeZone, Utc};
use tempfile::{TempDir, tempdir};
fn ts(n: i64) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
.single()
.expect("base ts")
+ chrono::Duration::seconds(n)
}
fn inference() -> SmInferenceConfig {
SmInferenceConfig {
context_token_budget: 1_000_000,
compressed_context_max_tokens: 1_000_000,
..SmInferenceConfig::default()
}
}
fn engine_with(window: u32, inf: &SmInferenceConfig) -> (SmContextEngine, TempDir) {
let dir = tempdir().expect("tempdir");
let rounds = SmRoundsConfig { window };
let eng = SmContextEngine::open("conv-1", dir.path(), inf, &rounds).expect("open engine");
(eng, dir)
}
#[tokio::test]
async fn window_evicts_oldest_round() {
let inf = inference();
let (mut eng, _dir) = engine_with(10, &inf);
let mock = MockProvider::fixed("summary");
let mut last_evicted = 0;
for i in 0..11 {
last_evicted = eng
.record(
&mock,
"claude-haiku",
format!("u{i}"),
format!("a{i}"),
ts(i),
Vec::new(),
)
.await
.expect("record");
}
assert_eq!(eng.conversation().window_len(), 10, "window capped at 10");
assert_eq!(eng.conversation().total_rounds, 11, "monotonic counter");
assert_eq!(last_evicted, 1, "the 11th round evicted exactly one");
assert_eq!(mock.requests().len(), 1, "exactly one compaction call");
}
#[tokio::test]
async fn evicted_content_lands_in_summary() {
let inf = inference();
let (mut eng, _dir) = engine_with(1, &inf);
let mock = MockProvider::fixed("FOLDED: round about goal g-99");
eng.record(&mock, "m", "first user", "first asst", ts(0), Vec::new())
.await
.expect("record 1");
eng.record(&mock, "m", "second user", "second asst", ts(1), Vec::new())
.await
.expect("record 2");
assert_eq!(
eng.conversation().compressed_context,
"FOLDED: round about goal g-99"
);
assert_eq!(eng.conversation().window_len(), 1);
}
#[tokio::test]
async fn golden_ids_survive_compaction() {
let inf = inference();
let (mut eng, _dir) = engine_with(1, &inf);
let mock = MockProvider::echo("SUMMARY> ");
eng.record(
&mock,
"m",
"please advance goal g-314",
"spawned session s-271 to do it",
ts(0),
vec![ToolTrace::new("session_new", "created s-271 for g-314")],
)
.await
.expect("record 1");
eng.record(&mock, "m", "status?", "in progress", ts(1), Vec::new())
.await
.expect("record 2");
let summary = &eng.conversation().compressed_context;
assert!(summary.contains("g-314"), "goal id survives: {summary}");
assert!(summary.contains("s-271"), "session id survives: {summary}");
}
#[tokio::test]
async fn token_budget_triggers_compaction() {
let mut inf = inference();
inf.context_token_budget = 50; inf.compressed_context_max_tokens = 1_000_000;
let (mut eng, _dir) = engine_with(10, &inf);
let mock = MockProvider::fixed("compacted");
eng.record(&mock, "m", "small", "small", ts(0), Vec::new())
.await
.expect("record small");
let huge = "x".repeat(4_000);
let evicted = eng
.record(&mock, "m", huge, "ok", ts(1), Vec::new())
.await
.expect("record huge");
assert!(evicted >= 1, "token-budget overflow evicted ≥1 round");
assert!(!mock.requests().is_empty(), "budget path called compaction");
assert!(
eng.conversation().token_estimate <= 1_000_000,
"estimate stays bounded"
);
}
#[tokio::test]
async fn default_compaction_uses_summary_model() {
let inf = inference();
let (mut eng, _dir) = engine_with(1, &inf);
let mock = MockProvider::fixed("s");
let haiku = "anthropic/claude-haiku";
eng.record(&mock, haiku, "u0", "a0", ts(0), Vec::new())
.await
.expect("r0");
eng.record(&mock, haiku, "u1", "a1", ts(1), Vec::new())
.await
.expect("r1");
assert_eq!(mock.last_model().as_deref(), Some(haiku));
}
#[tokio::test]
async fn compaction_model_override_is_honored() {
let inf = inference();
let (mut eng, _dir) = engine_with(1, &inf);
let mock = MockProvider::fixed("s");
let override_model = "openrouter/meta-llama/llama-3.1-8b-instruct:free";
eng.record(&mock, override_model, "u0", "a0", ts(0), Vec::new())
.await
.expect("r0");
eng.record(&mock, override_model, "u1", "a1", ts(1), Vec::new())
.await
.expect("r1");
assert_eq!(mock.last_model().as_deref(), Some(override_model));
}
#[tokio::test]
async fn state_file_written_each_record() {
let inf = inference();
let dir = tempdir().expect("tempdir");
let rounds = SmRoundsConfig { window: 10 };
let mut eng = SmContextEngine::open("conv-x", dir.path(), &inf, &rounds).expect("open");
let mock = MockProvider::fixed("s");
let store = ConversationStore::new(dir.path());
eng.record(&mock, "m", "u0", "a0", ts(0), Vec::new())
.await
.expect("r0");
assert!(store.path_for("conv-x").exists(), "file after first record");
eng.record(&mock, "m", "u1", "a1", ts(1), Vec::new())
.await
.expect("r1");
assert!(
store.path_for("conv-x").exists(),
"file after second record"
);
let resumed = SmContextEngine::open("conv-x", dir.path(), &inf, &rounds).expect("resume");
assert_eq!(resumed.conversation(), eng.conversation());
}
#[tokio::test]
async fn new_conversation_starts_empty() {
let inf = inference();
let (eng, _dir) = engine_with(10, &inf);
assert_eq!(eng.conversation(), &SmConversation::new());
}
#[tokio::test]
async fn engine_resumes_persisted_conversation() {
let inf = inference();
let dir = tempdir().expect("tempdir");
let rounds = SmRoundsConfig { window: 10 };
let mock = MockProvider::fixed("s");
let mut eng = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("open");
eng.record(&mock, "m", "u0", "a0", ts(0), Vec::new())
.await
.expect("r0");
let snapshot = eng.conversation().clone();
drop(eng);
let resumed = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("resume");
assert_eq!(resumed.conversation(), &snapshot);
}
#[tokio::test]
async fn oversized_summary_is_resummarised() {
let mut inf = inference();
inf.context_token_budget = 1_000_000;
inf.compressed_context_max_tokens = 1; let (mut eng, _dir) = engine_with(1, &inf);
let mock = MockProvider::echo("BLK> ");
eng.record(&mock, "m", "first with content", "reply", ts(0), Vec::new())
.await
.expect("r0");
eng.record(&mock, "m", "second", "reply2", ts(1), Vec::new())
.await
.expect("r1");
assert_eq!(mock.requests().len(), 2, "fold then resummarise");
let reqs = mock.requests();
assert_eq!(reqs[1].system, COMPRESS_SUMMARY_SYSTEM_PROMPT);
}
#[tokio::test]
async fn assembly_order_is_exact() {
let inf = inference();
let mock = MockProvider::fixed("s");
let (mut eng, _dir) = engine_with(1, &inf);
eng.record(&mock, "m", "old-u", "old-a", ts(0), Vec::new())
.await
.expect("r0");
eng.record(&mock, "m", "recent-u", "recent-a", ts(1), Vec::new())
.await
.expect("r1");
let msgs = eng.assemble_working_prompt("SYSTEM", Some("RECALL"), "CURRENT");
assert_eq!(msgs.len(), 4, "exact §7.5 message count (single system)");
let system_count = msgs.iter().filter(|m| m.role == "system").count();
assert_eq!(system_count, 1, "exactly one system message");
assert_eq!(msgs[0].role, "system");
let sys = &msgs[0].content;
let base = sys.find("SYSTEM").expect("base prompt present");
let earlier = sys
.find("Earlier in this conversation:")
.expect("compressed block present");
let recall = sys.find("Relevant memory:").expect("recall block present");
assert!(
base < earlier && earlier < recall,
"blocks ordered base < compressed < recall in: {sys}"
);
assert_eq!(msgs[1].role, "user");
assert_eq!(msgs[1].content, "recent-u");
assert_eq!(msgs[2].role, "assistant");
assert_eq!(msgs[2].content, "recent-a");
assert_eq!(msgs[3].role, "user");
assert_eq!(msgs[3].content, "CURRENT");
}
#[tokio::test]
async fn assembly_emits_single_system_message() {
let inf = inference();
let mock = MockProvider::fixed("compressed-summary");
let (mut eng, _dir) = engine_with(1, &inf);
eng.record(&mock, "m", "old-u", "old-a", ts(0), Vec::new())
.await
.expect("r0");
eng.record(&mock, "m", "recent-u", "recent-a", ts(1), Vec::new())
.await
.expect("r1");
let msgs = eng.assemble_working_prompt("BASE-PROMPT", Some("MEM"), "NOW");
let system_count = msgs.iter().filter(|m| m.role == "system").count();
assert_eq!(system_count, 1, "only one system message allowed");
let sys = &msgs[0].content;
assert!(sys.contains("BASE-PROMPT"), "base section present: {sys}");
assert!(
sys.contains("Earlier in this conversation: compressed-summary"),
"compressed section present: {sys}"
);
assert!(
sys.contains("Relevant memory: MEM"),
"recall section present: {sys}"
);
}
#[tokio::test]
async fn assembly_skips_empty_blocks() {
let inf = inference();
let (eng, _dir) = engine_with(10, &inf);
let msgs = eng.assemble_working_prompt("SYSTEM", None, "CURRENT");
assert_eq!(msgs.len(), 2, "only system + current");
assert_eq!(msgs[0].role, "system");
assert_eq!(msgs[0].content, "SYSTEM");
assert_eq!(msgs[1].role, "user");
assert_eq!(msgs[1].content, "CURRENT");
}
#[tokio::test]
async fn token_estimate_tracks_content() {
let inf = inference();
let (mut eng, _dir) = engine_with(10, &inf);
let mock = MockProvider::fixed("s");
eng.record(&mock, "m", "abcd", "efgh", ts(0), Vec::new())
.await
.expect("r0");
assert_eq!(eng.conversation().token_estimate, 2);
}
#[tokio::test]
async fn single_oversized_round_converges_within_budget() {
let mut inf = inference();
inf.context_token_budget = 50; inf.compressed_context_max_tokens = 1_000_000; let (mut eng, _dir) = engine_with(10, &inf);
let mock = MockProvider::fixed("tiny");
eng.record(&mock, "m", "small", "small", ts(0), Vec::new())
.await
.expect("record small");
let huge = "x".repeat(8_000);
eng.record(&mock, "m", huge, "ok", ts(1), Vec::new())
.await
.expect("record huge");
let budget = inf.context_token_budget as usize;
assert!(
eng.conversation().token_estimate <= budget,
"converged within budget: estimate={} budget={}",
eng.conversation().token_estimate,
budget
);
assert!(
eng.conversation().window_len() <= 1,
"huge round folded out of the verbatim window"
);
let store = ConversationStore::new(_dir.path());
let persisted = store.load(eng.conv_id()).expect("load persisted");
assert!(
persisted.token_estimate <= budget,
"persisted context within budget"
);
}
#[tokio::test]
async fn convergence_terminates_when_summariser_cannot_shrink() {
let mut inf = inference();
inf.context_token_budget = 1; inf.compressed_context_max_tokens = 1_000_000;
let (mut eng, _dir) = engine_with(10, &inf);
let mock = MockProvider::echo("ECHO> ");
eng.record(&mock, "m", "first", "first", ts(0), Vec::new())
.await
.expect("record first");
let huge = "y".repeat(4_000);
eng.record(&mock, "m", huge, "ok", ts(1), Vec::new())
.await
.expect("record huge terminates");
assert!(eng.conversation().window_len() <= 1, "window drained");
}
#[tokio::test]
async fn open_recomputes_stale_token_estimate() {
let inf = inference();
let dir = tempdir().expect("tempdir");
let rounds = SmRoundsConfig { window: 10 };
let mock = MockProvider::fixed("s");
let mut eng = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("open");
eng.record(&mock, "m", "abcd", "efgh", ts(0), Vec::new())
.await
.expect("r0");
drop(eng);
let store = ConversationStore::new(dir.path());
let path = store.path_for("c");
let raw = std::fs::read_to_string(&path).expect("read state");
let mut json: serde_json::Value = serde_json::from_str(&raw).expect("parse state");
json["token_estimate"] = serde_json::json!(999_999_999u64);
std::fs::write(&path, serde_json::to_string_pretty(&json).expect("ser")).expect("write state");
let reopened = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("reopen");
assert_eq!(
reopened.conversation().token_estimate,
2,
"estimate recomputed from content, not the stale 999_999_999"
);
}
#[tokio::test]
async fn loaded_stale_estimate_does_not_spuriously_compact() {
let mut inf = inference();
inf.context_token_budget = 1_000_000;
let dir = tempdir().expect("tempdir");
let rounds = SmRoundsConfig { window: 10 };
let seed = MockProvider::fixed("s");
let mut eng = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("open");
eng.record(&seed, "m", "u0", "a0", ts(0), Vec::new())
.await
.expect("seed round");
drop(eng);
let store = ConversationStore::new(dir.path());
let path = store.path_for("c");
let raw = std::fs::read_to_string(&path).expect("read state");
let mut json: serde_json::Value = serde_json::from_str(&raw).expect("parse");
json["token_estimate"] = serde_json::json!(5_000_000u64);
std::fs::write(&path, serde_json::to_string_pretty(&json).expect("ser")).expect("write");
let mut reopened = SmContextEngine::open("c", dir.path(), &inf, &rounds).expect("reopen");
let probe = MockProvider::fixed("should-not-be-called");
let evicted = reopened
.record(&probe, "m", "u1", "a1", ts(1), Vec::new())
.await
.expect("record after reload");
assert_eq!(evicted, 0, "no spurious eviction from a stale estimate");
assert!(
probe.requests().is_empty(),
"no spurious compaction call from a stale estimate"
);
}
#[tokio::test]
async fn record_without_compaction_persists_round_verbatim() {
let inf = inference();
let (mut eng, dir) = engine_with(10, &inf);
eng.record_without_compaction("hello sm", "hi operator", ts(1), Vec::new())
.expect("record without compaction");
assert_eq!(eng.conversation().total_rounds, 1);
assert_eq!(eng.conversation().window_len(), 1);
assert!(
eng.conversation().token_estimate > 0,
"token estimate updated for the recorded round"
);
let store = ConversationStore::new(dir.path());
let persisted = store.load(eng.conv_id()).expect("load persisted");
assert_eq!(persisted.total_rounds, 1);
assert_eq!(persisted.recent_rounds.len(), 1);
let round = &persisted.recent_rounds[0];
assert_eq!(round.user, "hello sm");
assert_eq!(round.assistant, "hi operator");
assert_eq!(round.ts, ts(1));
assert!(round.tool_calls.is_empty());
assert!(
persisted.compressed_context.is_empty(),
"no compaction without a provider"
);
}