use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use super::{
ProjectSessionIndex, SaveError, SessionState, normalized_safe_project_root, project_index_path,
};
use crate::core::policy::runtime::{TestPolicyOverride, lock_test_override};
use crate::core::session::admission::{self, StorageAdmissionProbe};
use crate::core::session::paths::sessions_dir;
use crate::core::session::{Finding, TaskInfo};
const CUSTOMER: &str = "CUSTOMER-482193";
const AWS_KEY: &str = concat!("AK", "IAIOSFODNN7EXAMPLE");
const BLOCK_CUSTOMER: &str = "name = \"session-boundary\"\nversion = \"1.0.0\"\ndescription = \"fixture\"\n[filters.blocked_patterns]\ncustomer = 'CUSTOMER-482193'\n";
fn fixture(project: &Path, id: &str) -> SessionState {
let root = project.to_string_lossy().into_owned();
let mut session = SessionState::new();
session.id = id.to_string();
session.project_root = Some(root.clone());
session.shell_cwd = Some(root);
session.task = Some(TaskInfo {
description: "storage boundary fixture".to_string(),
intent: None,
progress_pct: None,
});
session.next_steps.push(format!("publish {CUSTOMER}"));
session.stats.total_tool_calls = 3;
session.stats.unsaved_changes = 3;
session
}
fn project_policy(project: &Path, pack: &str) {
std::fs::create_dir_all(project.join(".lean-ctx")).expect("policy directory");
std::fs::write(project.join(".lean-ctx/policy.toml"), pack).expect("write project policy");
}
fn gateway_config(value: &str) {
let dir = crate::core::paths::config_dir_read_only().expect("isolated config directory");
std::fs::create_dir_all(&dir).expect("config directory");
std::fs::write(dir.join("config.toml"), value).expect("write gateway config");
}
fn collect(base: &Path, dir: &Path, found: &mut BTreeMap<String, Vec<u8>>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(base, &path, found);
continue;
}
let name = path
.strip_prefix(base)
.expect("entry below the session store")
.to_string_lossy()
.into_owned();
if Path::new(&name)
.extension()
.is_some_and(|extension| extension == "lock")
{
continue;
}
found.insert(name, std::fs::read(&path).expect("read store entry"));
}
}
fn published() -> BTreeMap<String, Vec<u8>> {
let sessions = sessions_dir().expect("sessions dir");
let mut found = BTreeMap::new();
collect(&sessions, &sessions, &mut found);
found
}
fn names(store: &BTreeMap<String, Vec<u8>>) -> Vec<&String> {
store.keys().collect()
}
fn assert_nothing_published(what: &str) {
let store = published();
assert!(store.is_empty(), "{what}: {:?}", names(&store));
}
const CHILD: &str = "LEAN_CTX_STORAGE_BOUNDARY_SUBPROCESS";
const CHILD_LIMIT: std::time::Duration = std::time::Duration::from_mins(2);
fn ran_in_its_own_process(name: &str) -> bool {
use std::io::{Read, Seek};
if std::env::var_os(CHILD).is_some() {
return false;
}
let _env = crate::core::data_dir::test_env_lock();
let environment = tempfile::tempdir().expect("child environment");
let mut output = tempfile::tempfile().expect("child output");
let exact = format!("core::session::persistence::storage_boundary_tests::{name}");
let mut command = std::process::Command::new(std::env::current_exe().expect("test binary"));
command
.args(["--exact", &exact, "--nocapture", "--test-threads=1"])
.env(CHILD, "1")
.env_remove("LEAN_CTX_PROJECT_ROOT")
.env_remove("CLAUDE_PROJECT_DIR")
.env_remove("WORKSPACE_FOLDER_PATHS")
.stdin(std::process::Stdio::null())
.stdout(output.try_clone().expect("child stdout"))
.stderr(output.try_clone().expect("child stderr"));
for directory in [
"LEAN_CTX_DATA_DIR",
"LEAN_CTX_CONFIG_DIR",
"LEAN_CTX_STATE_DIR",
"LEAN_CTX_CACHE_DIR",
] {
command.env(directory, environment.path());
}
let mut child = command.spawn().expect("start the isolated test");
let deadline = std::time::Instant::now() + CHILD_LIMIT;
let status = loop {
match child.try_wait().expect("wait for the isolated test") {
Some(status) => break Some(status),
None if std::time::Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
break None;
}
None => std::thread::sleep(std::time::Duration::from_millis(20)),
}
};
let mut bytes = Vec::new();
output.rewind().expect("child output");
output.read_to_end(&mut bytes).expect("child output");
let text = String::from_utf8_lossy(&bytes);
let status = status.unwrap_or_else(|| panic!("{exact} did not end in {CHILD_LIMIT:?}\n{text}"));
assert!(
status.success(),
"{exact} failed in its own process\n{text}"
);
assert!(
text.contains("test result: ok. 1 passed"),
"{exact} did not run in its own process\n{text}"
);
true
}
fn assert_absent_everywhere(store: &BTreeMap<String, Vec<u8>>, needle: &str) {
for (name, bytes) in store {
assert!(
!String::from_utf8_lossy(bytes).contains(needle),
"{name} carries content the current policy withholds"
);
}
}
fn lock_probe(
owner: &Arc<tokio::sync::RwLock<SessionState>>,
) -> (StorageAdmissionProbe, Rc<RefCell<Vec<bool>>>) {
let lock_free = Rc::new(RefCell::new(Vec::new()));
let probe = StorageAdmissionProbe::install({
let owner = Arc::clone(owner);
let lock_free = Rc::clone(&lock_free);
move || lock_free.borrow_mut().push(owner.try_write().is_ok())
});
(probe, lock_free)
}
#[test]
fn prepare_under_the_session_write_lock_runs_no_storage_admission() {
if ran_in_its_own_process("prepare_under_the_session_write_lock_runs_no_storage_admission") {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let owner = Arc::new(tokio::sync::RwLock::new(fixture(
project.path(),
"boundary-lock",
)));
let (_probe, lock_free) = lock_probe(&owner);
let prepared = {
let mut live = owner.try_write().expect("uncontested session");
let prepared = live.prepare_save().expect("prepare under the write guard");
assert!(
lock_free.borrow().is_empty(),
"prepare_save ran storage admission (configuration and policy refresh) while the session write lock was held"
);
prepared
};
prepared
.publish()
.expect("publish after the guard is released");
let lock_free = lock_free.borrow();
assert!(
!lock_free.is_empty(),
"publication must run storage admission"
);
assert!(
lock_free.iter().all(|free| *free),
"storage admission must only run while the session write lock is free"
);
}
#[test]
fn unavailable_project_policy_is_reported_after_the_session_lock_is_released() {
if ran_in_its_own_process(
"unavailable_project_policy_is_reported_after_the_session_lock_is_released",
) {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let owner = Arc::new(tokio::sync::RwLock::new(fixture(
project.path(),
"boundary-refresh-unavailable",
)));
std::fs::remove_dir_all(project.path()).expect("delete project root");
let (_probe, lock_free) = lock_probe(&owner);
let prepared = {
let mut live = owner.try_write().expect("uncontested session");
let prepared = live
.prepare_save()
.expect("prepare_save must not consult the project root or its policy");
assert!(lock_free.borrow().is_empty());
prepared
};
let error = prepared
.publish()
.expect_err("a session of a missing project root is withheld");
assert!(
matches!(error, SaveError::AdmissionRefused(_)),
"unexpected refusal: {error:?}"
);
assert!(!lock_free.borrow().is_empty() && lock_free.borrow().iter().all(|free| *free));
assert_nothing_published("a refused save writes nothing");
}
#[test]
fn prepared_save_is_not_published_after_its_project_root_is_deleted() {
if ran_in_its_own_process("prepared_save_is_not_published_after_its_project_root_is_deleted") {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-root-deleted");
let index_path = project_index_path(
&sessions_dir().expect("sessions dir"),
&normalized_safe_project_root(&project.path().to_string_lossy()).expect("safe root"),
);
let prepared = session
.prepare_save()
.expect("prepare while the root exists");
std::fs::remove_dir_all(project.path()).expect("delete project root");
let outcome = prepared.write_to_disk_with_policy(false);
assert!(
matches!(outcome.result, Err(SaveError::AdmissionRefused(_))),
"unexpected outcome: {:?}",
outcome.result
);
let sessions = sessions_dir().expect("sessions dir");
assert!(!sessions.join("boundary-root-deleted.json").exists());
assert!(!sessions.join("boundary-root-deleted_snapshot.txt").exists());
assert!(!sessions.join("latest.json").exists());
assert!(!index_path.exists());
assert_nothing_published("nothing at all may be written");
assert!(session.acknowledge_save(outcome).is_err());
assert!(session.stats.unsaved_changes >= 3);
assert!(session.last_save_failed);
assert!(session.should_save());
}
#[test]
fn admitted_save_is_refused_at_commit_after_its_project_root_is_deleted() {
if ran_in_its_own_process(
"admitted_save_is_refused_at_commit_after_its_project_root_is_deleted",
) {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-root-revoked");
session.save().expect("first save under the existing root");
let before = published();
assert!(before.contains_key("boundary-root-revoked.json"));
assert!(before.contains_key("latest.json"));
session.next_steps.push("second revision".to_string());
session.increment();
let admitted = session
.prepare_save()
.expect("prepare")
.admit()
.expect("admitted while the root exists");
std::fs::remove_dir_all(project.path()).expect("delete project root");
let error = admitted
.write_to_disk()
.expect_err("the admitting authority no longer holds");
assert!(
matches!(error, SaveError::AuthorityChanged(_)),
"unexpected refusal: {error:?}"
);
assert_eq!(
published(),
before,
"primary file, pointer, snapshot and index must be unchanged"
);
}
#[test]
fn admitted_save_is_refused_at_commit_after_the_project_policy_is_tightened() {
if ran_in_its_own_process(
"admitted_save_is_refused_at_commit_after_the_project_policy_is_tightened",
) {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _live = lock_test_override();
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-policy-tightened");
let admitted = session
.prepare_save()
.expect("prepare")
.admit()
.expect("admitted while no pack withholds the content");
project_policy(project.path(), BLOCK_CUSTOMER);
let error = admitted
.write_to_disk()
.expect_err("content admitted under the previous policy must not be published");
assert!(
matches!(error, SaveError::AuthorityChanged(_)),
"unexpected refusal: {error:?}"
);
assert_nothing_published("nothing may be written");
}
#[test]
fn prepared_save_never_publishes_content_a_tightened_project_policy_withholds() {
if ran_in_its_own_process(
"prepared_save_never_publishes_content_a_tightened_project_policy_withholds",
) {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _live = lock_test_override();
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-policy-late");
let prepared = session.prepare_save().expect("prepare");
project_policy(project.path(), BLOCK_CUSTOMER);
let result = prepared.publish();
let store = published();
assert_absent_everywhere(&store, CUSTOMER);
if result.is_ok() {
let stored = SessionState::load_by_id("boundary-policy-late")
.expect("the admitted remainder is published");
assert!(stored.next_steps.is_empty());
} else {
assert!(
store.is_empty(),
"{}: {:?}",
"a refused save writes nothing",
names(&store)
);
}
}
#[test]
fn admitted_save_is_refused_at_commit_after_the_gateway_is_tightened() {
if ran_in_its_own_process("admitted_save_is_refused_at_commit_after_the_gateway_is_tightened") {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
gateway_config("[context_gateway]\nenabled = false\n");
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-gateway-tightened");
session.findings.push(Finding {
file: None,
line: None,
summary: format!("key is {AWS_KEY}"),
timestamp: chrono::Utc::now(),
});
let admitted = session
.prepare_save()
.expect("prepare")
.admit()
.expect("admitted while the Gateway is disabled");
let late = session.prepare_save().expect("prepare a second copy");
gateway_config("[context_gateway]\nmode = \"governed\"\n");
let error = admitted
.write_to_disk()
.expect_err("content admitted under the previous Gateway policy must not be published");
assert!(
matches!(error, SaveError::AuthorityChanged(_)),
"unexpected refusal: {error:?}"
);
assert_nothing_published("nothing may be written");
let result = late.publish();
let store = published();
assert_absent_everywhere(&store, AWS_KEY);
if result.is_err() {
assert!(
store.is_empty(),
"{}: {:?}",
"a refused save writes nothing",
names(&store)
);
}
}
#[test]
fn competitor_that_misses_the_project_index_lock_writes_nothing_and_stays_pending() {
if ran_in_its_own_process(
"competitor_that_misses_the_project_index_lock_writes_nothing_and_stays_pending",
) {
return;
}
use fs2::FileExt;
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-index-lock-busy");
let index_path = project_index_path(
&sessions_dir().expect("sessions dir"),
&normalized_safe_project_root(&project.path().to_string_lossy()).expect("safe root"),
);
std::fs::create_dir_all(index_path.parent().expect("index directory"))
.expect("create index directory");
let holder = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(index_path.with_extension("lock"))
.expect("project index lock file");
holder
.lock_exclusive()
.expect("hold the project index lock");
let outcome = session
.prepare_save()
.expect("prepare")
.write_to_disk_with_policy(false);
assert!(
matches!(
&outcome.result,
Err(SaveError::Storage(message)) if message.contains("project index lock")
),
"unexpected outcome: {:?}",
outcome.result
);
assert_nothing_published("the competitor writes nothing");
assert!(session.acknowledge_save(outcome).is_err());
assert!(session.stats.unsaved_changes >= 3, "changes stay pending");
assert!(session.last_save_failed);
assert!(session.should_save());
FileExt::unlock(&holder).expect("release");
session
.save()
.expect("the retry publishes once the lock is free");
assert!(published().contains_key("boundary-index-lock-busy.json"));
assert_eq!(session.stats.unsaved_changes, 0);
}
#[test]
fn unchanged_authority_publishes_the_pre_change_bytes() {
if ran_in_its_own_process("unchanged_authority_publishes_the_pre_change_bytes") {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let mut session = fixture(project.path(), "boundary-positive");
let canonical_root =
normalized_safe_project_root(&project.path().to_string_lossy()).expect("safe root");
let projection = admission::for_storage(&session).expect("storage projection");
let (expected_json, _) = projection.storage_json().expect("storage json");
let expected_snapshot = projection.build_compaction_snapshot();
assert!(expected_json.contains(CUSTOMER));
session
.prepare_save()
.expect("prepare")
.publish()
.expect("unchanged authority publishes");
let sessions = sessions_dir().expect("sessions dir");
let read = |name: &str| std::fs::read(sessions.join(name)).expect("published artifact");
assert_eq!(read("boundary-positive.json"), expected_json.as_bytes());
assert_eq!(read("latest.json"), br#"{"id":"boundary-positive"}"#);
assert_eq!(
read("boundary-positive_snapshot.txt"),
expected_snapshot.as_bytes()
);
let index: ProjectSessionIndex = serde_json::from_slice(
&std::fs::read(project_index_path(&sessions, &canonical_root)).expect("project index"),
)
.expect("valid project index");
assert_eq!(index.version, 1);
assert_eq!(index.project_root, canonical_root);
assert_eq!(index.session_ids, ["boundary-positive"]);
let after_first = published();
session
.prepare_save()
.expect("prepare")
.admit()
.expect("admit")
.write_to_disk()
.expect("commit under the unchanged authority");
assert_eq!(published(), after_first);
session.save().expect("synchronous save");
assert_eq!(session.stats.unsaved_changes, 0);
assert!(!session.last_save_failed);
}
#[test]
fn background_save_adapter_publishes_then_keeps_changes_pending_without_a_root() {
if ran_in_its_own_process(
"background_save_adapter_publishes_then_keeps_changes_pending_without_a_root",
) {
return;
}
let _data = crate::core::data_dir::isolated_data_dir();
let _community = TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project root");
let owner = Arc::new(tokio::sync::RwLock::new(fixture(
project.path(),
"boundary-background",
)));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime
.block_on(SessionState::save_shared(Arc::clone(&owner)))
.expect("background save under the existing root");
let before = published();
assert!(before.contains_key("boundary-background.json"));
assert_eq!(
owner.try_read().expect("uncontested").stats.unsaved_changes,
0
);
{
let mut live = owner.try_write().expect("uncontested");
live.next_steps.push("second revision".to_string());
live.increment();
}
std::fs::remove_dir_all(project.path()).expect("delete project root");
let error = runtime
.block_on(SessionState::save_shared(Arc::clone(&owner)))
.expect_err("a session of a deleted project root is withheld");
assert!(!error.is_empty());
assert_eq!(published(), before, "the refused save changes nothing");
let live = owner.try_read().expect("uncontested");
assert_ne!(live.stats.unsaved_changes, 0, "changes stay pending");
assert!(live.last_save_failed);
}