use crate::memory::lesson::{save_lesson, SaveLessonRequest};
use crate::memory::suppression::{create_suppression, parse_target, SuppressRequest};
use rusqlite::Connection;
use super::super::policy::{ContextLimits, ContextPolicy};
use super::super::query::{load_context_data, load_context_data_with_policy};
use super::{insert_global_memory, insert_memory, insert_memory_with_branch, setup_context_schema};
#[test]
fn load_context_data_records_primary_memory_query_failures() {
let conn = match Connection::open_in_memory() {
Ok(conn) => conn,
Err(error) => panic!("in-memory connection should open: {error}"),
};
setup_context_schema(&conn);
conn.execute("DROP TABLE memories", []).unwrap();
let policy = ContextPolicy::from_limits(ContextLimits::default());
let loaded = load_context_data_with_policy(&conn, "/tmp/remem", None, &policy, true);
assert!(loaded.memories.is_empty());
let memory_errors = loaded
.errors
.iter()
.filter(|error| error.section == "memories")
.collect::<Vec<_>>();
assert_eq!(memory_errors.len(), 2);
assert!(memory_errors.iter().any(|error| error
.message
.contains("failed to load recent context memories")));
assert!(memory_errors.iter().any(|error| error
.message
.contains("failed to retrieve hybrid context memories")));
}
#[test]
fn load_context_data_records_lesson_query_failures() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
conn.execute("DROP TABLE memory_lessons", []).unwrap();
let policy = ContextPolicy::from_limits(ContextLimits::default());
let loaded = load_context_data_with_policy(&conn, "/tmp/remem", None, &policy, true);
assert!(loaded.lessons.is_empty());
assert_eq!(loaded.errors.len(), 1);
assert_eq!(loaded.errors[0].section, "lessons");
assert!(loaded.errors[0]
.message
.contains("failed to load lessons for /tmp/remem"));
}
#[test]
fn load_context_data_records_summary_query_failures() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
conn.execute("DROP TABLE session_summaries", []).unwrap();
let policy = ContextPolicy::from_limits(ContextLimits::default());
let loaded = load_context_data_with_policy(&conn, "/tmp/remem", None, &policy, true);
assert!(loaded.summaries.is_empty());
assert_eq!(loaded.errors.len(), 1);
assert_eq!(loaded.errors[0].section, "sessions");
assert!(loaded.errors[0]
.message
.contains("failed to load recent summaries for /tmp/remem"));
}
#[test]
fn load_context_data_records_workstream_query_failures() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
conn.execute("DROP TABLE workstreams", []).unwrap();
let policy = ContextPolicy::from_limits(ContextLimits::default());
let loaded = load_context_data_with_policy(&conn, "/tmp/remem", None, &policy, true);
assert!(loaded.workstreams.is_empty());
assert_eq!(loaded.errors.len(), 1);
assert_eq!(loaded.errors[0].section, "workstreams");
assert!(loaded.errors[0]
.message
.contains("failed to load active workstreams for /tmp/remem"));
}
#[test]
fn load_context_data_dedupes_generated_topic_keys_by_workstream_context() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
for idx in 0..6 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("decision-{:016x}", idx)),
"decision",
&format!("Landing page decision {idx}"),
"[Context: Build VibeGuard landing page and wireframe variants]\nDecision body",
now - idx,
);
}
insert_memory(
&conn,
99,
project,
Some("post-write-hook-worktree-hang"),
"bugfix",
"Fix post-write hook worktree hang",
"Handle .git files when resolving worktree roots",
now - 100,
);
let loaded = load_context_data(&conn, project, None);
let landing_count = loaded
.memories
.iter()
.filter(|memory| memory.title.contains("Landing page decision"))
.count();
assert_eq!(landing_count, 1);
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Fix post-write hook worktree hang"));
}
#[test]
fn load_context_data_clusters_contexts_by_pr_reference() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
insert_memory(
&conn,
1,
project,
Some("decision-1111111111111111"),
"decision",
"Skill install decision A",
"[Context: Review VibeGuard PR #116 added agentsmd-audit and trajectory-review skills]\nBody",
now,
);
insert_memory(
&conn,
2,
project,
Some("decision-2222222222222222"),
"decision",
"Skill install decision B",
"[Context: Review VibeGuard PR 116 new skill installation contract]\nBody",
now - 1,
);
let loaded = load_context_data(&conn, project, None);
let pr_decision_count = loaded
.memories
.iter()
.filter(|memory| memory.title.starts_with("Skill install decision"))
.count();
assert_eq!(pr_decision_count, 1);
}
#[test]
fn load_context_data_excludes_policy_suppressed_memories() -> anyhow::Result<()> {
let conn = Connection::open_in_memory()?;
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
insert_memory(
&conn,
1,
project,
Some("visible-context"),
"decision",
"Visible context memory",
"Visible context memory should be injected.",
now,
);
insert_memory(
&conn,
2,
project,
Some("hidden-context"),
"decision",
"Hidden context memory",
"Hidden context memory should not be injected.",
now + 1,
);
create_suppression(
&conn,
&SuppressRequest {
target: parse_target("memory:2")?,
reason: Some("not useful"),
actor: Some("test"),
},
)?;
let loaded = load_context_data(&conn, project, None);
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Visible context memory"));
assert!(!loaded
.memories
.iter()
.any(|memory| memory.title == "Hidden context memory"));
Ok(())
}
#[test]
fn load_context_data_prefers_current_branch_within_dedup_cluster() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
insert_memory_with_branch(
&conn,
1,
project,
Some("decision-1111111111111111"),
"decision",
"Main branch landing decision",
"[Context: Build VibeGuard landing page and wireframe variants]\nMain body",
now,
Some("main"),
);
insert_memory_with_branch(
&conn,
2,
project,
Some("decision-2222222222222222"),
"decision",
"Feature branch landing decision",
"[Context: Build VibeGuard landing page and wireframe variants]\nFeature body",
now - 10,
Some("fix/context-selection"),
);
let loaded = load_context_data(&conn, project, Some("fix/context-selection"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Feature branch landing decision"));
assert!(!loaded
.memories
.iter()
.any(|memory| memory.title == "Main branch landing decision"));
}
#[test]
fn load_context_data_keeps_current_branch_memories_before_limit() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
for idx in 0..55 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("branchless-topic-{idx}")),
"decision",
&format!("Branchless decision {idx}"),
"Recent branchless decision body",
now - idx,
);
}
insert_memory_with_branch(
&conn,
200,
project,
Some("current-branch-topic"),
"bugfix",
"Current branch operational fix",
"Older but branch-specific fix body",
now - 1_000,
Some("fix/context-selection"),
);
let loaded = load_context_data(&conn, project, Some("fix/context-selection"));
assert!(loaded.memories.len() > ContextLimits::default().memory_index_limit);
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Current branch operational fix"));
}
#[test]
fn load_context_data_limits_memory_self_diagnostics_before_index_rendering() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
for idx in 0..8 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("decision-{:016x}", idx)),
"decision",
&format!("Memory injection diagnosis {idx}"),
"Debug remem context SessionStart memories loaded behavior",
now - idx,
);
}
insert_memory(
&conn,
100,
project,
Some("guard-paths-source-path"),
"bugfix",
"Fix guard path source selection",
"Use source path evidence in guard output",
now - 20,
);
let loaded = load_context_data(&conn, project, None);
let self_diagnostic_count = loaded
.memories
.iter()
.filter(|memory| memory.title.contains("Memory injection diagnosis"))
.count();
assert_eq!(self_diagnostic_count, 2);
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Fix guard path source selection"));
}
#[test]
fn load_context_data_excludes_preferences_from_main_memory_pool() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/computer";
let now = chrono::Utc::now().timestamp();
for idx in 0..60 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("preference-topic-{idx}")),
"preference",
&format!("Preference {idx}"),
"User prefers evidence-backed coordination",
now - idx,
);
}
insert_memory(
&conn,
100,
project,
Some("context-budget-decision"),
"decision",
"Use host-aware context compiler",
"Split preferences from the main memory index",
now - 100,
);
insert_memory(
&conn,
101,
project,
Some("context-budget-discovery"),
"discovery",
"Preference flood starves core memories",
"The main index was dominated by preferences",
now - 101,
);
let loaded = load_context_data(&conn, project, None);
assert!(!loaded
.memories
.iter()
.any(|memory| memory.memory_type == "preference"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Use host-aware context compiler"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Preference flood starves core memories"));
}
#[test]
fn load_context_data_excludes_preferences_before_candidate_limit() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/computer";
let now = chrono::Utc::now().timestamp();
for idx in 0..130 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("preference-topic-{idx}")),
"preference",
&format!("Preference {idx}"),
"User prefers evidence-backed coordination",
now - idx,
);
}
insert_memory(
&conn,
200,
project,
Some("context-budget-decision"),
"decision",
"Use host-aware context compiler",
"Split preferences from the main memory index",
now - 1_000,
);
insert_memory(
&conn,
201,
project,
Some("context-budget-bugfix"),
"bugfix",
"Keep core memories visible",
"Filter preferences before applying the candidate cap",
now - 1_001,
);
let loaded = load_context_data(&conn, project, None);
assert!(loaded
.memories
.iter()
.all(|memory| memory.memory_type != "preference"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Use host-aware context compiler"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Keep core memories visible"));
}
#[test]
fn load_context_data_excludes_deleted_and_rejected_memories() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
for (idx, title, status) in [
(1, "Deleted memory", "deleted"),
(2, "Rejected memory", "rejected"),
(3, "Active memory", "active"),
] {
insert_memory(
&conn,
idx,
project,
Some(&format!("governance-status-{idx}")),
"decision",
title,
"Context should include only active governed memories.",
now + idx,
);
if status != "active" {
conn.execute(
"UPDATE memories SET status = ?1 WHERE id = ?2",
rusqlite::params![status, idx],
)
.unwrap();
}
}
let loaded = load_context_data(&conn, project, None);
let titles: Vec<_> = loaded
.memories
.iter()
.map(|memory| memory.title.as_str())
.collect();
assert_eq!(titles, vec!["Active memory"]);
}
#[test]
fn load_context_data_loads_lessons_separately_from_main_memory_pool() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
save_lesson(
&conn,
&SaveLessonRequest {
session_id: Some("s1"),
project,
topic_key: Some("lesson-build-loop"),
title: "Stop build-fix loops",
content: "Lesson: after repeated build failures, stop and challenge the hypothesis.",
confidence: 0.9,
source_evidence: Some("build failed repeatedly"),
files: None,
branch: None,
scope: "project",
created_at_epoch: Some(now),
stale_after_epoch: None,
},
)
.unwrap();
insert_memory(
&conn,
200,
project,
Some("context-budget-decision"),
"decision",
"Keep context bounded",
"Separate lessons from the normal memory index",
now - 1,
);
let loaded = load_context_data(&conn, project, None);
assert_eq!(loaded.lessons.len(), 1);
assert_eq!(loaded.lessons[0].memory.title, "Stop build-fix loops");
assert!(loaded
.memories
.iter()
.all(|memory| memory.memory_type != "lesson"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Keep context bounded"));
}
#[test]
fn load_context_data_filters_lessons_by_current_branch() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
for (title, branch) in [
("Main lesson", Some("main")),
("Feature lesson", Some("feature/context")),
] {
save_lesson(
&conn,
&SaveLessonRequest {
session_id: Some("s1"),
project,
topic_key: Some(&title.replace(' ', "-").to_lowercase()),
title,
content:
"Lesson: branch-specific lessons should follow the current context branch.",
confidence: 0.9,
source_evidence: None,
files: None,
branch,
scope: "project",
created_at_epoch: None,
stale_after_epoch: None,
},
)
.unwrap();
}
let loaded = load_context_data(&conn, project, Some("main"));
let titles: Vec<_> = loaded
.lessons
.iter()
.map(|lesson| lesson.memory.title.as_str())
.collect();
assert_eq!(titles, vec!["Main lesson"]);
}
#[test]
fn load_context_data_filters_lessons_before_candidate_limit_and_keeps_core_memory() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
let limits = ContextLimits {
candidate_fetch_limit: 1,
lesson_limit: 10,
memory_index_limit: 10,
core_item_limit: 3,
..ContextLimits::default()
};
let policy = ContextPolicy::from_limits(limits);
for idx in 0..20 {
save_lesson(
&conn,
&SaveLessonRequest {
session_id: Some("s1"),
project,
topic_key: Some(&format!("low-confidence-lesson-{idx}")),
title: &format!("Low confidence lesson {idx}"),
content: "Lesson: this should not enter context because confidence is too low.",
confidence: 0.2,
source_evidence: None,
files: None,
branch: None,
scope: "project",
created_at_epoch: Some(now + idx),
stale_after_epoch: None,
},
)
.unwrap();
}
insert_memory(
&conn,
200,
project,
Some("keep-core-decision"),
"decision",
"Keep core decision visible",
"Low-confidence lessons must not crowd out core decisions.",
now - 1_000,
);
let loaded = load_context_data_with_policy(&conn, project, None, &policy, true);
assert!(loaded.lessons.is_empty());
assert!(loaded
.memories
.iter()
.all(|memory| memory.memory_type != "lesson"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Keep core decision visible"));
}
#[test]
fn load_context_data_excludes_global_non_preferences_from_main_memory_pool() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
insert_global_memory(
&conn,
1,
"manual",
Some("mihomo-proxy-group-duplicate-name"),
"bugfix",
"Mihomo proxy group duplicate name",
"This global bugfix should not appear in project SessionStart Core or Index.",
now,
);
insert_memory(
&conn,
2,
project,
Some("context-project-only-memory"),
"decision",
"Keep context memory project scoped",
"SessionStart should load project-local non-preference memories.",
now - 1,
);
let loaded = load_context_data(&conn, project, None);
assert!(!loaded
.memories
.iter()
.any(|memory| memory.title == "Mihomo proxy group duplicate name"));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Keep context memory project scoped"));
}
#[test]
fn load_context_data_excludes_global_non_preferences_from_hybrid_retrieval() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/remem";
let now = chrono::Utc::now().timestamp();
let limits = ContextLimits {
candidate_fetch_limit: 1,
memory_index_limit: 10,
core_item_limit: 3,
..ContextLimits::default()
};
let policy = ContextPolicy::from_limits(limits);
insert_memory(
&conn,
1,
project,
Some("older-local-memory"),
"decision",
"Older local remem decision",
"A project-local decision should still appear through hybrid retrieval.",
now - 10,
);
insert_memory(
&conn,
2,
project,
Some("newer-local-memory"),
"decision",
"Newer local context decision",
"This fills the tiny recent candidate limit.",
now,
);
for idx in 0..25 {
insert_global_memory(
&conn,
idx + 3,
"manual",
Some(&format!("global-remem-memory-{idx}")),
"bugfix",
&format!("Global remem bugfix {idx}"),
"A global result matching the basename query should not enter context.",
now + 1 + idx,
);
}
let loaded = load_context_data_with_policy(&conn, project, None, &policy, true);
assert!(!loaded
.memories
.iter()
.any(|memory| memory.title.starts_with("Global remem bugfix")));
assert!(loaded
.memories
.iter()
.any(|memory| memory.title == "Older local remem decision"));
}
#[test]
fn context_limits_env_override_and_legacy_alias_are_respected() {
let mut vars = std::collections::HashMap::new();
vars.insert("REMEM_CONTEXT_OBSERVATIONS", "7".to_string());
vars.insert("REMEM_CONTEXT_CORE_ITEM_LIMIT", "3".to_string());
vars.insert("REMEM_CONTEXT_PREFERENCE_CHAR_LIMIT", "900".to_string());
let limits = ContextLimits::from_env_reader(|key| vars.get(key).cloned());
assert_eq!(limits.memory_index_limit, 7);
assert_eq!(limits.core_item_limit, 3);
assert_eq!(limits.preference_char_limit, 900);
}
#[test]
fn context_limits_new_memory_index_env_wins_over_legacy_alias() {
let mut vars = std::collections::HashMap::new();
vars.insert("REMEM_CONTEXT_OBSERVATIONS", "7".to_string());
vars.insert("REMEM_CONTEXT_MEMORY_INDEX_LIMIT", "11".to_string());
let limits = ContextLimits::from_env_reader(|key| vars.get(key).cloned());
assert_eq!(limits.memory_index_limit, 11);
}
#[test]
fn context_limits_relevance_k_defaults_overrides_disables_and_rejects_invalid() {
let defaults = ContextLimits::from_env_reader(|_| None);
assert_eq!(defaults.sessionstart_relevance_k, 1);
let overridden = ContextLimits::from_env_reader(|key| {
(key == "REMEM_CONTEXT_RELEVANCE_K").then(|| "5".to_string())
});
assert_eq!(overridden.sessionstart_relevance_k, 5);
let disabled = ContextLimits::from_env_reader(|key| {
(key == "REMEM_CONTEXT_RELEVANCE_K").then(|| "0".to_string())
});
assert_eq!(disabled.sessionstart_relevance_k, 0);
let invalid = ContextLimits::from_env_reader(|key| {
(key == "REMEM_CONTEXT_RELEVANCE_K").then(|| "invalid".to_string())
});
assert_eq!(invalid.sessionstart_relevance_k, 1);
}
#[test]
fn load_context_data_keeps_core_candidates_when_index_limit_is_small() {
let conn = Connection::open_in_memory().unwrap();
setup_context_schema(&conn);
let project = "/tmp/vibeguard";
let now = chrono::Utc::now().timestamp();
let limits = ContextLimits {
memory_index_limit: 1,
core_item_limit: 4,
..ContextLimits::default()
};
let policy = ContextPolicy::from_limits(limits);
for idx in 0..8 {
insert_memory(
&conn,
idx + 1,
project,
Some(&format!("decision-topic-{idx}")),
"decision",
&format!("Decision {idx}"),
"Decision body",
now - idx,
);
}
let loaded = load_context_data_with_policy(&conn, project, None, &policy, true);
assert!(loaded.memories.len() > limits.memory_index_limit);
assert!(loaded.memories.len() >= limits.core_item_limit);
}