use super::*;
struct RestoreConfig(Option<std::ffi::OsString>);
impl Drop for RestoreConfig {
fn drop(&mut self) {
match self.0.take() {
Some(value) => std::env::set_var(crate::config::CONFIG_DIR_ENV, value),
None => std::env::remove_var(crate::config::CONFIG_DIR_ENV),
}
}
}
#[tokio::test]
async fn selected_root_survives_build_runtime_actions_and_both_reload_host_paths() {
let temporary = tempfile::tempdir().unwrap();
let env_root = temporary.path().join("env sessions");
let _env = crate::config::test_support::SessionDirEnv::new(Some(&env_root));
let _config = RestoreConfig(std::env::var_os(crate::config::CONFIG_DIR_ENV));
let agent = temporary.path().join("agent");
std::fs::create_dir_all(&agent).unwrap();
std::env::set_var(crate::config::CONFIG_DIR_ENV, &agent);
let settings_path = agent.join("settings.json");
for mode in ["flag", "settings", "environment"] {
let cwd = temporary.path().join(mode);
std::fs::create_dir_all(&cwd).unwrap();
let root = if mode == "flag" {
cwd.join("../flag selected sessions")
} else {
temporary.path().join(format!("{mode} selected sessions"))
};
let settings = match mode {
"environment" => serde_json::json!({}),
"flag" => {
serde_json::json!({"sessionDir": temporary.path().join("ignored settings root")})
}
_ => serde_json::json!({"sessionDir": root}),
};
std::fs::write(&settings_path, settings.to_string()).unwrap();
std::env::set_var(
"RPI_CODING_AGENT_SESSION_DIR",
if mode == "environment" {
&root
} else {
&env_root
},
);
let resolved = crate::provider::resolve(
Some("anthropic"),
Some(crate::provider::DEFAULT_MODEL_ID),
None,
Some("test-key"),
None,
)
.unwrap();
let args = Args {
session_dir: (mode == "flag")
.then(|| std::path::PathBuf::from("../flag selected sessions")),
no_extensions: true,
no_tools: true,
no_skills: true,
no_prompt_templates: true,
no_context_files: true,
system_prompt: Some("test".into()),
..Default::default()
};
let prepared = prepare_extensions(&args, &cwd, false).unwrap();
assert_eq!(prepared.session_dir, root);
let changed_root = temporary.path().join(format!("{mode} changed sessions"));
std::env::set_var("RPI_CODING_AGENT_SESSION_DIR", &changed_root);
std::fs::write(
&settings_path,
serde_json::json!({"sessionDir": changed_root}).to_string(),
)
.unwrap();
let (harness, _events, context) = build_prepared(&resolved, &args, &cwd, false, prepared)
.await
.unwrap();
assert_eq!(context.session_dir(), root);
let initial_id = harness.session().storage().metadata().id.clone();
harness.session().set_name(Some("original")).await.unwrap();
let original_host = context
.action_bridge
.lock()
.unwrap()
.as_ref()
.unwrap()
.clone_host();
let fork_id = original_host.fork(serde_json::json!({})).await.unwrap()["sessionId"]
.as_str()
.unwrap()
.to_owned();
assert_ne!(fork_id, initial_id);
assert!(
!reload_extension_resources(&harness, &context)
.await
.had_errors
);
let host = context
.action_bridge
.lock()
.unwrap()
.as_ref()
.unwrap()
.clone_host();
host.switch_session(serde_json::json!({"id":initial_id}))
.await
.unwrap();
assert_eq!(
harness.session().get_name().await.unwrap().as_deref(),
Some("original")
);
context.action_bridge.lock().unwrap().take();
assert!(
!reload_extension_resources(&harness, &context)
.await
.had_errors
);
let host = context
.action_bridge
.lock()
.unwrap()
.as_ref()
.unwrap()
.clone_host();
let new_id = host.new_session(serde_json::json!({})).await.unwrap()["sessionId"]
.as_str()
.unwrap()
.to_owned();
assert_eq!(harness.session().storage().metadata().id, new_id);
host.switch_session(serde_json::json!({"id":fork_id}))
.await
.unwrap();
let metadata = list_session_metadata_in_dir(&cwd.to_string_lossy(), &root)
.await
.unwrap();
assert_eq!(metadata.len(), 3, "{mode}");
for meta in metadata {
let lock = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(format!("{}.lock", meta.path))
.unwrap();
assert!(fs2::FileExt::try_lock_exclusive(&lock).is_err());
}
assert!(
!changed_root.exists(),
"{mode}: runtime must not drift to the changed root"
);
assert!(!cwd.join(".rpi/sessions").exists());
}
}
#[tokio::test]
async fn resume_picker_reports_the_selected_root_when_it_has_no_history() {
let temporary = tempfile::tempdir().unwrap();
let root = temporary.path().join("custom empty sessions");
let error = crate::resume_picker::select_in_dir(temporary.path(), &root)
.await
.unwrap_err();
assert!(error.contains(&root.display().to_string()));
create_jsonl_session(&root, &temporary.path().to_string_lossy())
.await
.unwrap();
let error = crate::resume_picker::select_in_dir(temporary.path(), &root)
.await
.unwrap_err();
assert!(error.contains("no resumable sessions"));
assert!(error.contains(&root.display().to_string()));
}
#[tokio::test]
async fn importing_a_torn_tail_repairs_only_the_private_copy() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path().to_string_lossy();
let source_root = temporary.path().join("source");
let session = create_jsonl_session(&source_root, &cwd).await.unwrap();
session.set_name(Some("preserved")).await.unwrap();
let path = list_session_files_sync(&source_root).remove(0);
let mut content = std::fs::read(&path).unwrap();
content.extend_from_slice(b"{\"kind\":");
std::fs::write(&path, &content).unwrap();
let root = temporary.path().join("destination");
let imported = import_session_file(&path, &cwd, &root).await.unwrap();
assert_eq!(
imported.get_name().await.unwrap().as_deref(),
Some("preserved")
);
assert_eq!(std::fs::read(&path).unwrap(), content);
let saved = list_session_files_sync(&root).remove(0);
assert!(!std::fs::read_to_string(saved)
.unwrap()
.ends_with("{\"kind\":"));
}