use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde_json::{Value, json};
use crate::core::house_rules::{
HouseRulesQuotaInput, house_rules_quota, select_within_house_rules_quota,
};
use crate::db::{CreateWorkspaceInput, DbConnection, StoredMemory};
pub use crate::models::GLOBAL_MEMORY_SCHEMA_V1;
pub const GLOBAL_MEMORY_DISABLED_CODE: &str = "global_lane_disabled";
pub const GLOBAL_STORE_NEEDS_MIGRATION_MARKER: &str = "needs migration";
pub const GLOBAL_PROVENANCE_LANE: &str = "global";
pub const GLOBAL_STORE_DIRNAME: &str = "global";
pub const DEFAULT_USER_DATA_SUFFIX: &str = ".local/share/ee";
pub const DEFAULT_GLOBAL_FAN_IN_BASIS_POINTS: u32 = 1_500;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GlobalStorePaths {
pub root: PathBuf,
pub database_path: PathBuf,
pub index_dir: PathBuf,
}
impl GlobalStorePaths {
#[must_use]
pub fn from_data_root(user_data_root: &Path) -> Self {
Self::from_root(&user_data_root.join(GLOBAL_STORE_DIRNAME))
}
#[must_use]
pub fn from_root(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
database_path: root.join("ee.db"),
index_dir: root.join("indexes"),
}
}
#[must_use]
pub fn resolve(user_data_root: &Path, configured_root: Option<&Path>) -> Self {
match configured_root {
Some(root) => Self::from_root(root),
None => Self::from_data_root(user_data_root),
}
}
}
#[must_use]
pub fn default_user_data_root_from_values(
xdg_data_home: Option<&OsStr>,
home: Option<&OsStr>,
) -> Option<PathBuf> {
xdg_data_home
.filter(|value| !value.is_empty())
.map(|root| PathBuf::from(root).join("ee"))
.or_else(|| {
home.filter(|value| !value.is_empty())
.map(|root| PathBuf::from(root).join(DEFAULT_USER_DATA_SUFFIX))
})
}
pub fn default_user_data_root_from_env() -> Result<PathBuf, String> {
default_user_data_root_from_values(
std::env::var_os("XDG_DATA_HOME").as_deref(),
std::env::var_os("HOME").as_deref(),
)
.ok_or_else(|| "global memory store requires XDG_DATA_HOME or HOME".to_owned())
}
pub fn default_global_store_paths_from_env() -> Result<GlobalStorePaths, String> {
default_user_data_root_from_env().map(|root| GlobalStorePaths::from_data_root(&root))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GlobalInclusionReason {
Included,
DisabledByConfig,
DisabledByFlag,
NotParticipating,
StoreAbsent,
}
impl GlobalInclusionReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Included => "included",
Self::DisabledByConfig => "disabled_by_config",
Self::DisabledByFlag => "disabled_by_flag",
Self::NotParticipating => "not_participating",
Self::StoreAbsent => "store_absent",
}
}
#[must_use]
pub const fn degraded_code(self) -> Option<&'static str> {
match self {
Self::Included => None,
_ => Some(GLOBAL_MEMORY_DISABLED_CODE),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalInclusionDecision {
pub included: bool,
pub reason: GlobalInclusionReason,
}
#[derive(Clone, Copy, Debug)]
pub struct GlobalInclusionInput {
pub store_present: bool,
pub participating: bool,
pub config_enabled: bool,
pub no_global_flag: bool,
}
#[must_use]
pub fn resolve_global_inclusion(input: &GlobalInclusionInput) -> GlobalInclusionDecision {
let reason = if !input.store_present {
GlobalInclusionReason::StoreAbsent
} else if !input.participating {
GlobalInclusionReason::NotParticipating
} else if !input.config_enabled {
GlobalInclusionReason::DisabledByConfig
} else if input.no_global_flag {
GlobalInclusionReason::DisabledByFlag
} else {
GlobalInclusionReason::Included
};
GlobalInclusionDecision {
included: matches!(reason, GlobalInclusionReason::Included),
reason,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryLane {
Workspace,
Team,
Global,
}
impl MemoryLane {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Team => "team",
Self::Global => "global",
}
}
}
#[must_use]
pub const fn lane_specificity_rank(lane: MemoryLane) -> u8 {
match lane {
MemoryLane::Workspace => 0,
MemoryLane::Team => 1,
MemoryLane::Global => 2,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LaneCandidate {
pub id: String,
pub lane: MemoryLane,
pub conflict_key: String,
pub content_hash: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LaneConflictKind {
Corroboration,
Contradiction,
}
impl LaneConflictKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Corroboration => "corroboration",
Self::Contradiction => "contradiction",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LaneConflict {
pub conflict_key: String,
pub kind: LaneConflictKind,
pub workspace_id: String,
pub global_id: String,
pub both_surfaced: bool,
pub workspace_overrides: bool,
}
impl LaneConflict {
#[must_use]
pub fn data_json(&self) -> Value {
json!({
"conflictKey": self.conflict_key,
"kind": self.kind.as_str(),
"workspaceId": self.workspace_id,
"globalId": self.global_id,
"bothSurfaced": self.both_surfaced,
"workspaceOverrides": self.workspace_overrides,
})
}
}
#[must_use]
pub fn surface_lane_conflicts(candidates: &[LaneCandidate]) -> Vec<LaneConflict> {
let mut by_key: BTreeMap<&str, (Vec<&LaneCandidate>, Vec<&LaneCandidate>)> = BTreeMap::new();
for candidate in candidates {
let entry = by_key.entry(candidate.conflict_key.as_str()).or_default();
match candidate.lane {
MemoryLane::Workspace => entry.0.push(candidate),
MemoryLane::Global => entry.1.push(candidate),
MemoryLane::Team => {}
}
}
let mut out = Vec::new();
for (key, (workspace_rows, global_rows)) in by_key {
for workspace in &workspace_rows {
for global in &global_rows {
let kind = if workspace.content_hash == global.content_hash {
LaneConflictKind::Corroboration
} else {
LaneConflictKind::Contradiction
};
out.push(LaneConflict {
conflict_key: key.to_owned(),
kind,
workspace_id: workspace.id.clone(),
global_id: global.id.clone(),
both_surfaced: true,
workspace_overrides: matches!(kind, LaneConflictKind::Corroboration),
});
}
}
}
out.sort_by(|a, b| {
a.conflict_key
.cmp(&b.conflict_key)
.then_with(|| a.workspace_id.cmp(&b.workspace_id))
.then_with(|| a.global_id.cmp(&b.global_id))
});
out
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrecedenceConflict {
pub conflict_key: String,
pub kind: LaneConflictKind,
pub more_specific_lane: MemoryLane,
pub more_specific_id: String,
pub less_specific_lane: MemoryLane,
pub less_specific_id: String,
pub both_surfaced: bool,
pub more_specific_overrides: bool,
}
impl PrecedenceConflict {
#[must_use]
pub fn data_json(&self) -> Value {
json!({
"conflictKey": self.conflict_key,
"kind": self.kind.as_str(),
"moreSpecificLane": self.more_specific_lane.as_str(),
"moreSpecificId": self.more_specific_id,
"lessSpecificLane": self.less_specific_lane.as_str(),
"lessSpecificId": self.less_specific_id,
"bothSurfaced": self.both_surfaced,
"moreSpecificOverrides": self.more_specific_overrides,
})
}
}
#[must_use]
pub fn surface_precedence_conflicts(candidates: &[LaneCandidate]) -> Vec<PrecedenceConflict> {
let mut by_key: BTreeMap<&str, BTreeMap<MemoryLane, Vec<&LaneCandidate>>> = BTreeMap::new();
for candidate in candidates {
by_key
.entry(candidate.conflict_key.as_str())
.or_default()
.entry(candidate.lane)
.or_default()
.push(candidate);
}
let lanes = [MemoryLane::Workspace, MemoryLane::Team, MemoryLane::Global];
let mut out = Vec::new();
for (key, by_lane) in by_key {
for (left_index, left_lane) in lanes.iter().enumerate() {
for right_lane in lanes.iter().skip(left_index.saturating_add(1)) {
let Some(left_rows) = by_lane.get(left_lane) else {
continue;
};
let Some(right_rows) = by_lane.get(right_lane) else {
continue;
};
for left in left_rows {
for right in right_rows {
let (more, less) = (*left, *right);
let kind = if more.content_hash == less.content_hash {
LaneConflictKind::Corroboration
} else {
LaneConflictKind::Contradiction
};
out.push(PrecedenceConflict {
conflict_key: key.to_owned(),
kind,
more_specific_lane: more.lane,
more_specific_id: more.id.clone(),
less_specific_lane: less.lane,
less_specific_id: less.id.clone(),
both_surfaced: true,
more_specific_overrides: matches!(
kind,
LaneConflictKind::Corroboration
),
});
}
}
}
}
}
out.sort_by(|left, right| {
left.conflict_key
.cmp(&right.conflict_key)
.then_with(|| left.more_specific_id.cmp(&right.more_specific_id))
.then_with(|| left.less_specific_id.cmp(&right.less_specific_id))
.then_with(|| {
left.more_specific_lane
.as_str()
.cmp(right.more_specific_lane.as_str())
})
.then_with(|| {
left.less_specific_lane
.as_str()
.cmp(right.less_specific_lane.as_str())
})
});
out
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GlobalFanIn {
pub cap_tokens: u64,
pub enabled: bool,
pub selected: Vec<usize>,
}
#[must_use]
pub fn bounded_global_fan_in(
global_item_token_costs: &[u64],
total_budget_tokens: u64,
quota_basis_points: u32,
opted_out: bool,
) -> GlobalFanIn {
let quota = house_rules_quota(&HouseRulesQuotaInput {
total_budget_tokens,
quota_basis_points,
workspace_opted_out: opted_out,
});
let selected = if quota.enabled {
select_within_house_rules_quota(global_item_token_costs, quota.cap_tokens)
} else {
Vec::new()
};
GlobalFanIn {
cap_tokens: quota.cap_tokens,
enabled: quota.enabled,
selected,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GlobalMemoryStoreMetadata {
pub database_path: String,
pub index_dir: String,
pub present: bool,
pub enabled: bool,
pub participating: bool,
pub schema_version: String,
pub memory_count: u64,
pub last_modified: Option<String>,
}
impl GlobalMemoryStoreMetadata {
#[must_use]
pub fn data_json(&self) -> Value {
json!({
"schema": GLOBAL_MEMORY_SCHEMA_V1,
"databasePath": self.database_path,
"indexDir": self.index_dir,
"present": self.present,
"enabled": self.enabled,
"participating": self.participating,
"schemaVersion": self.schema_version,
"memoryCount": self.memory_count,
"lastModified": self.last_modified,
})
}
}
pub const GLOBAL_WORKSPACE_NAME: &str = "ee-global";
#[must_use]
pub fn global_workspace_key(paths: &GlobalStorePaths) -> String {
paths.root.to_string_lossy().into_owned()
}
#[must_use]
pub fn global_workspace_id(paths: &GlobalStorePaths) -> String {
crate::core::curate::stable_workspace_id(&paths.root)
}
pub fn open_or_create_global_store(
paths: &GlobalStorePaths,
) -> Result<(DbConnection, String), String> {
std::fs::create_dir_all(&paths.root).map_err(|error| {
format!(
"failed to create global store directory {}: {error}",
paths.root.display()
)
})?;
let connection = DbConnection::open_file(&paths.database_path)
.map_err(|error| format!("failed to open global store database: {error}"))?;
connection
.migrate()
.map_err(|error| format!("failed to migrate global store database: {error}"))?;
let workspace_id = ensure_global_workspace_row(&connection, paths)?;
Ok((connection, workspace_id))
}
fn ensure_global_workspace_row(
connection: &DbConnection,
paths: &GlobalStorePaths,
) -> Result<String, String> {
let workspace_key = global_workspace_key(paths);
let requested = global_workspace_id(paths);
if let Some(existing) = crate::core::workspace::select_existing_workspace_row(
connection,
&requested,
&[paths.root.as_path()],
)
.map_err(|error| format!("failed to check global workspace row: {}", error.message()))?
{
return Ok(existing.id);
}
connection
.insert_workspace(
&requested,
&CreateWorkspaceInput {
path: workspace_key,
name: Some(GLOBAL_WORKSPACE_NAME.to_owned()),
},
)
.map_err(|error| format!("failed to create global workspace row: {error}"))?;
Ok(requested)
}
pub fn read_global_store_memories(
paths: &GlobalStorePaths,
include_tombstoned: bool,
) -> Result<Vec<StoredMemory>, String> {
if !paths.database_path.exists() {
return Ok(Vec::new());
}
let connection = DbConnection::open_file_read_only(&paths.database_path)
.map_err(|error| format!("failed to open global store database read-only: {error}"))?;
let needs_migration = connection
.needs_migration()
.map_err(|error| format!("failed to inspect global store migration state: {error}"))?;
if needs_migration {
return Err(
"global store database needs migration; skipping read-only global tier".to_owned(),
);
}
let requested = global_workspace_id(paths);
let Some(workspace) = crate::core::workspace::select_existing_workspace_row(
&connection,
&requested,
&[paths.root.as_path()],
)
.map_err(|error| {
format!(
"failed to resolve global workspace row: {}",
error.message()
)
})?
else {
return Ok(Vec::new());
};
connection
.list_memories(&workspace.id, None, include_tombstoned)
.map_err(|error| format!("failed to list global store memories: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn path_safe_tempdir(prefix: &str) -> Result<tempfile::TempDir, String> {
let system_temp = Path::new("/tmp");
if system_temp.is_dir() {
let root =
std::fs::canonicalize(system_temp).unwrap_or_else(|_| system_temp.to_path_buf());
return tempfile::Builder::new()
.prefix(prefix)
.tempdir_in(root)
.map_err(|error| error.to_string());
}
tempfile::Builder::new()
.prefix(prefix)
.tempdir()
.map_err(|error| error.to_string())
}
#[test]
fn global_store_create_is_idempotent_and_persists_memories() -> Result<(), String> {
use crate::db::CreateMemoryInput;
let tempdir = path_safe_tempdir("ee-global-store.")?;
let paths = GlobalStorePaths::from_root(&tempdir.path().join("global"));
assert!(read_global_store_memories(&paths, false)?.is_empty());
let (connection, workspace_id) = open_or_create_global_store(&paths)?;
assert!(paths.database_path.exists());
assert_eq!(workspace_id, global_workspace_id(&paths));
connection
.insert_memory(
&crate::testing::mem("global"),
&CreateMemoryInput {
workspace_id: workspace_id.clone(),
level: "semantic".to_owned(),
kind: "note".to_owned(),
content: "always run cargo fmt --check before release".to_owned(),
workflow_id: None,
confidence: 0.9,
utility: 0.0,
importance: 0.0,
provenance_uri: None,
trust_class: "human_explicit".to_owned(),
trust_subclass: None,
tags: vec!["global".to_owned()],
valid_from: None,
valid_to: None,
},
)
.map_err(|error| error.to_string())?;
drop(connection);
let (again, workspace_id_again) = open_or_create_global_store(&paths)?;
assert_eq!(workspace_id, workspace_id_again);
drop(again);
let memories = read_global_store_memories(&paths, false)?;
assert_eq!(memories.len(), 1);
assert_eq!(
memories[0].content,
"always run cargo fmt --check before release"
);
assert_eq!(memories[0].workspace_id, workspace_id);
Ok(())
}
#[test]
fn read_global_store_memories_reports_pending_migration_without_writes() -> Result<(), String> {
let tempdir = path_safe_tempdir("ee-global-store-stale.")?;
let paths = GlobalStorePaths::from_root(&tempdir.path().join("global"));
std::fs::create_dir_all(&paths.root).map_err(|error| error.to_string())?;
{
let connection =
DbConnection::open_file(&paths.database_path).map_err(|error| error.to_string())?;
assert!(
!connection
.migration_table_exists()
.map_err(|error| error.to_string())?,
"fresh database must not start with a migration table"
);
}
let Err(error) = read_global_store_memories(&paths, false) else {
return Err("stale global store read must report pending migration".to_owned());
};
assert!(
error.contains(GLOBAL_STORE_NEEDS_MIGRATION_MARKER),
"the read path's pending-migration error must keep containing \
`{GLOBAL_STORE_NEEDS_MIGRATION_MARKER}`; core::search classifies \
the skipped global lane by that substring. Got {error}"
);
let connection = DbConnection::open_file_read_only(&paths.database_path)
.map_err(|error| error.to_string())?;
assert!(
connection
.needs_migration()
.map_err(|error| error.to_string())?,
"read-only global store read must leave migration state pending"
);
assert!(
!connection
.migration_table_exists()
.map_err(|error| error.to_string())?,
"read-only global store read must not create migration metadata"
);
Ok(())
}
fn candidate(id: &str, lane: MemoryLane, key: &str, hash: &str) -> LaneCandidate {
LaneCandidate {
id: id.to_owned(),
lane,
conflict_key: key.to_owned(),
content_hash: hash.to_owned(),
}
}
#[test]
fn paths_derive_under_global_subdir_of_data_root() {
let paths = GlobalStorePaths::from_data_root(Path::new("/home/agent/.local/share/ee"));
assert_eq!(
paths.root,
PathBuf::from("/home/agent/.local/share/ee/global")
);
assert_eq!(
paths.database_path,
PathBuf::from("/home/agent/.local/share/ee/global/ee.db")
);
assert_eq!(
paths.index_dir,
PathBuf::from("/home/agent/.local/share/ee/global/indexes")
);
}
#[test]
fn resolve_honors_configured_root_override() {
let from_root =
GlobalStorePaths::resolve(Path::new("/data"), Some(Path::new("/custom/global")));
assert_eq!(
from_root.database_path,
PathBuf::from("/custom/global/ee.db")
);
let from_default = GlobalStorePaths::resolve(Path::new("/data"), None);
assert_eq!(
from_default.database_path,
PathBuf::from("/data/global/ee.db")
);
}
#[test]
fn inclusion_decision_matrix_is_explainable() {
let on = resolve_global_inclusion(&GlobalInclusionInput {
store_present: true,
participating: true,
config_enabled: true,
no_global_flag: false,
});
assert!(on.included);
assert_eq!(on.reason, GlobalInclusionReason::Included);
assert_eq!(on.reason.degraded_code(), None);
let absent = resolve_global_inclusion(&GlobalInclusionInput {
store_present: false,
participating: false,
config_enabled: false,
no_global_flag: true,
});
assert!(!absent.included);
assert_eq!(absent.reason, GlobalInclusionReason::StoreAbsent);
let opted_out = resolve_global_inclusion(&GlobalInclusionInput {
store_present: true,
participating: false,
config_enabled: true,
no_global_flag: false,
});
assert_eq!(opted_out.reason, GlobalInclusionReason::NotParticipating);
let config_off = resolve_global_inclusion(&GlobalInclusionInput {
store_present: true,
participating: true,
config_enabled: false,
no_global_flag: false,
});
assert_eq!(config_off.reason, GlobalInclusionReason::DisabledByConfig);
let flag_off = resolve_global_inclusion(&GlobalInclusionInput {
store_present: true,
participating: true,
config_enabled: true,
no_global_flag: true,
});
assert_eq!(flag_off.reason, GlobalInclusionReason::DisabledByFlag);
for reason in [
GlobalInclusionReason::DisabledByConfig,
GlobalInclusionReason::DisabledByFlag,
GlobalInclusionReason::NotParticipating,
GlobalInclusionReason::StoreAbsent,
] {
assert_eq!(reason.degraded_code(), Some(GLOBAL_MEMORY_DISABLED_CODE));
}
}
#[test]
fn identical_content_across_lanes_is_corroboration_workspace_wins() {
let conflicts = surface_lane_conflicts(&[
candidate("ws1", MemoryLane::Workspace, "fmt-before-release", "h1"),
candidate("gl1", MemoryLane::Global, "fmt-before-release", "h1"),
]);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].kind, LaneConflictKind::Corroboration);
assert!(
conflicts[0].both_surfaced,
"global row stays as corroboration"
);
assert!(
conflicts[0].workspace_overrides,
"workspace row wins on identical content"
);
}
#[test]
fn divergent_content_across_lanes_is_a_surfaced_contradiction() {
let conflicts = surface_lane_conflicts(&[
candidate(
"ws1",
MemoryLane::Workspace,
"rebase-policy",
"rebase-always",
),
candidate("gl1", MemoryLane::Global, "rebase-policy", "rebase-never"),
]);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].kind, LaneConflictKind::Contradiction);
assert!(conflicts[0].both_surfaced);
assert!(
!conflicts[0].workspace_overrides,
"a contradiction must route to review, not auto-resolve by lane"
);
}
#[test]
fn unrelated_subjects_do_not_conflict() {
let conflicts = surface_lane_conflicts(&[
candidate("ws1", MemoryLane::Workspace, "subject-a", "h1"),
candidate("gl1", MemoryLane::Global, "subject-b", "h2"),
]);
assert!(conflicts.is_empty());
}
#[test]
fn empty_conflict_input_returns_empty_marker_list() {
let conflicts = surface_lane_conflicts(&[]);
assert!(
conflicts.is_empty(),
"empty candidate sets must not fabricate global/workspace conflicts"
);
}
#[test]
fn conflict_surfacing_is_insertion_order_independent() {
let forward = surface_lane_conflicts(&[
candidate("ws1", MemoryLane::Workspace, "k", "a"),
candidate("gl1", MemoryLane::Global, "k", "b"),
candidate("gl2", MemoryLane::Global, "k", "a"),
]);
let reversed = surface_lane_conflicts(&[
candidate("gl2", MemoryLane::Global, "k", "a"),
candidate("gl1", MemoryLane::Global, "k", "b"),
candidate("ws1", MemoryLane::Workspace, "k", "a"),
]);
assert_eq!(forward, reversed, "conflict output must be deterministic");
assert_eq!(forward.len(), 2, "one corroboration + one contradiction");
}
#[test]
fn lane_specificity_is_workspace_then_team_then_global() {
assert!(
lane_specificity_rank(MemoryLane::Workspace) < lane_specificity_rank(MemoryLane::Team)
);
assert!(
lane_specificity_rank(MemoryLane::Team) < lane_specificity_rank(MemoryLane::Global)
);
}
#[test]
fn identical_content_across_workspace_and_team_is_local_override() {
let conflicts = surface_precedence_conflicts(&[
candidate("ws1", MemoryLane::Workspace, "fmt-before-release", "h1"),
candidate("tm1", MemoryLane::Team, "fmt-before-release", "h1"),
]);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].kind, LaneConflictKind::Corroboration);
assert_eq!(conflicts[0].more_specific_lane, MemoryLane::Workspace);
assert_eq!(conflicts[0].less_specific_lane, MemoryLane::Team);
assert!(conflicts[0].both_surfaced);
assert!(
conflicts[0].more_specific_overrides,
"local workspace wins on overlap with team"
);
}
#[test]
fn identical_content_across_team_and_global_is_team_override() {
let conflicts = surface_precedence_conflicts(&[
candidate("tm1", MemoryLane::Team, "fmt-before-release", "h1"),
candidate("gl1", MemoryLane::Global, "fmt-before-release", "h1"),
]);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].more_specific_lane, MemoryLane::Team);
assert_eq!(conflicts[0].less_specific_lane, MemoryLane::Global);
assert!(conflicts[0].more_specific_overrides);
}
#[test]
fn workspace_team_contradiction_does_not_auto_resolve() {
let conflicts = surface_precedence_conflicts(&[
candidate(
"ws1",
MemoryLane::Workspace,
"rebase-policy",
"rebase-always",
),
candidate("tm1", MemoryLane::Team, "rebase-policy", "rebase-never"),
]);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].kind, LaneConflictKind::Contradiction);
assert!(conflicts[0].both_surfaced);
assert!(
!conflicts[0].more_specific_overrides,
"cross-lane contradiction must not silently pick a winner"
);
}
#[test]
fn three_lane_overlap_is_insertion_order_independent() {
let forward = surface_precedence_conflicts(&[
candidate("ws1", MemoryLane::Workspace, "k", "a"),
candidate("tm1", MemoryLane::Team, "k", "a"),
candidate("gl1", MemoryLane::Global, "k", "b"),
]);
let reversed = surface_precedence_conflicts(&[
candidate("gl1", MemoryLane::Global, "k", "b"),
candidate("tm1", MemoryLane::Team, "k", "a"),
candidate("ws1", MemoryLane::Workspace, "k", "a"),
]);
assert_eq!(forward, reversed);
assert_eq!(forward.len(), 3);
assert!(forward.iter().any(|conflict| conflict.more_specific_lane
== MemoryLane::Workspace
&& conflict.less_specific_lane == MemoryLane::Team
&& conflict.kind == LaneConflictKind::Corroboration));
assert!(
forward
.iter()
.any(|conflict| conflict.kind == LaneConflictKind::Contradiction
&& !conflict.more_specific_overrides)
);
}
#[test]
fn fan_in_respects_bounded_budget() {
let fan = bounded_global_fan_in(
&[100, 100, 40],
1_000,
DEFAULT_GLOBAL_FAN_IN_BASIS_POINTS,
false,
);
assert!(fan.enabled);
assert_eq!(fan.cap_tokens, 150);
assert_eq!(fan.selected, vec![0, 2]);
}
#[test]
fn fan_in_opt_out_selects_nothing() {
let fan =
bounded_global_fan_in(&[10, 10], 10_000, DEFAULT_GLOBAL_FAN_IN_BASIS_POINTS, true);
assert!(!fan.enabled);
assert_eq!(fan.cap_tokens, 0);
assert!(fan.selected.is_empty());
}
#[test]
fn fan_in_zero_budget_selects_nothing_even_when_enabled() {
let fan = bounded_global_fan_in(&[1, 2, 3], 0, DEFAULT_GLOBAL_FAN_IN_BASIS_POINTS, false);
assert!(fan.enabled);
assert_eq!(fan.cap_tokens, 0);
assert!(
fan.selected.is_empty(),
"zero-token packs cannot admit global memories"
);
}
#[test]
fn store_metadata_json_is_stable_and_redaction_safe() {
let meta = GlobalMemoryStoreMetadata {
database_path: "/home/agent/.local/share/ee/global/ee.db".to_owned(),
index_dir: "/home/agent/.local/share/ee/global/indexes".to_owned(),
present: true,
enabled: true,
participating: true,
schema_version: "v77".to_owned(),
memory_count: 12,
last_modified: Some("2026-06-17T00:00:00Z".to_owned()),
};
let value = meta.data_json();
assert_eq!(value["schema"], GLOBAL_MEMORY_SCHEMA_V1);
assert_eq!(value["memoryCount"], 12);
assert_eq!(value["participating"], true);
assert_eq!(value["databasePath"], meta.database_path);
assert!(value.get("content").is_none());
}
#[test]
fn provenance_lane_label_is_global() {
assert_eq!(GLOBAL_PROVENANCE_LANE, "global");
}
}