use super::*;
fn state_with_root(root: &std::path::Path) -> Arc<TuiState> {
let mut state = test_tui_state();
Arc::get_mut(&mut state).unwrap().session_dir = root.to_path_buf();
state
}
fn assert_session_locked(path: &str) {
let lock = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(format!("{path}.lock"))
.unwrap();
assert!(
fs2::FileExt::try_lock_exclusive(&lock).is_err(),
"{path} must remain exclusively locked"
);
}
fn empty_catalog() -> Arc<std::sync::RwLock<Vec<rpi_ai::Model>>> {
Arc::new(std::sync::RwLock::new(Vec::new()))
}
#[tokio::test]
async fn tui_fork_locks_the_copy_and_keeps_the_selected_root_for_switch_and_picker() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path().join("project");
std::fs::create_dir_all(&cwd).unwrap();
let root = temporary.path().join("custom sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
source
.append_message(user_message_with_images("custom-root history", vec![]))
.await
.unwrap();
let initial_id = source.storage().metadata().id.clone();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(source).await.unwrap();
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
fork_session(&harness, &cwd, &state.chat_container, &state, &context).await;
let fork_id = harness.session().storage().metadata().id.clone();
assert_ne!(fork_id, initial_id);
let metadata = crate::session::list_session_metadata_in_dir(&cwd.to_string_lossy(), &root)
.await
.unwrap();
assert_eq!(metadata.len(), 2);
for row in &metadata {
assert_session_locked(&row.path);
}
let lane: Arc<dyn AgentLane> = harness.lane("main");
assert!(
switch_to_session(
&harness,
&lane,
&initial_id,
&cwd,
&state.chat_container,
&state,
&context,
&empty_catalog()
)
.await
);
assert_eq!(harness.session().storage().metadata().id, initial_id);
let rendered = strip_ansi(&state.chat_container.render(160).join("\n"));
assert!(rendered.contains("custom-root history"));
let tui = Arc::new(TuiAltScreen::new(
Box::new(ProcessTerminal::new()),
true,
None,
));
let editor_container = Arc::new(Container::new());
let (tx, mut rx) = mpsc::unbounded_channel();
open_session_selector(&state, &editor_container, &state.editor, &tui, &tx);
assert!(
state.selector_open(),
"picker must find sessions in the selected root"
);
let router = state
.active_selector
.lock()
.unwrap()
.as_ref()
.unwrap()
.0
.clone();
router.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let Some(TuiMessage::SwitchSession(selected)) =
tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
.await
.unwrap()
else {
panic!("session selection expected")
};
assert!(metadata.iter().any(|row| row.path.contains(&selected)));
assert!(!cwd.join(".rpi/sessions").exists());
}
#[tokio::test]
async fn tui_import_preserves_identity_history_and_source_across_cwds() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path().join("project");
let old_cwd = temporary.path().join("original project");
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&old_cwd).unwrap();
let root = temporary.path().join("custom sessions");
let source_root = temporary.path().join("source");
let source = crate::session::create_jsonl_session(&source_root, &old_cwd.to_string_lossy())
.await
.unwrap();
source.set_name(Some("Imported history")).await.unwrap();
source
.append_message(user_message_with_images(
"preserved imported prompt",
vec![],
))
.await
.unwrap();
let expected = source.storage().metadata().id.clone();
let source_path = crate::session::list_session_files_sync(&source_root).remove(0);
let original = std::fs::read(&source_path).unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
import_session(
&harness,
&lane,
&source_path.to_string_lossy(),
&cwd,
&state.chat_container,
&state,
&context,
&empty_catalog(),
)
.await;
assert_eq!(harness.session().storage().metadata().id, expected);
assert_eq!(
harness.session().get_name().await.unwrap().as_deref(),
Some("Imported history")
);
assert!(strip_ansi(&state.chat_container.render(160).join("\n"))
.contains("preserved imported prompt"));
let metadata = crate::session::list_session_metadata_in_dir(&cwd.to_string_lossy(), &root)
.await
.unwrap();
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].cwd, old_cwd.to_string_lossy());
assert_session_locked(&metadata[0].path);
let restored =
crate::session::open_session_by_id_in_dir(&expected, &cwd.to_string_lossy(), &root)
.await
.map_err(|error| error.to_string())
.unwrap();
assert_eq!(
restored.get_name().await.unwrap().as_deref(),
Some("Imported history")
);
let imported = std::fs::read(&metadata[0].path).unwrap();
assert_eq!(imported, original);
for path in [&source_path, &std::path::PathBuf::from(&metadata[0].path)] {
assert!(
crate::session::import_session_file(path, &cwd.to_string_lossy(), &root)
.await
.err()
.unwrap()
.contains("already exists")
);
}
assert_eq!(std::fs::read(&source_path).unwrap(), original);
assert_eq!(std::fs::read(&metadata[0].path).unwrap(), imported);
assert_eq!(crate::session::list_session_files_sync(&root).len(), 1);
}
#[tokio::test]
async fn invalid_import_leaves_the_active_session_and_repository_untouched() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("selected");
let invalid = cwd.join("invalid.jsonl");
std::fs::write(&invalid, "not a session\n").unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
let initial = harness.session().storage().metadata().id.clone();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
import_session(
&harness,
&lane,
&invalid.to_string_lossy(),
cwd,
&state.chat_container,
&state,
&context,
&empty_catalog(),
)
.await;
assert_eq!(harness.session().storage().metadata().id, initial);
assert!(crate::session::list_session_files_sync(&root).is_empty());
assert!(
std::fs::read_dir(&root).unwrap().next().is_none(),
"staging files must be cleaned up"
);
assert_eq!(std::fs::read_to_string(invalid).unwrap(), "not a session\n");
}
#[tokio::test]
async fn session_switch_banner_reports_real_capabilities() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
std::fs::create_dir_all(&root).unwrap();
let first = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let first_id = first.storage().metadata().id.clone();
first
.append_message(user_message_with_images("first session history", vec![]))
.await
.unwrap();
let second = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let second_id = second.storage().metadata().id.clone();
assert_ne!(first_id, second_id);
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(second).await.unwrap();
harness
.set_tools(
vec![rpi_harness::types::HarnessTool::new(Arc::new(
BannerProbeTool::new("probe-tool"),
))],
None,
)
.await
.unwrap();
harness
.set_resources(rpi_harness::types::AgentHarnessResources {
skills: Some(vec![rpi_harness::types::Skill {
name: "demo-skill".into(),
description: "a skill the banner must advertise".into(),
content: String::new(),
file_path: "/skills/demo-skill/SKILL.md".into(),
disable_model_invocation: None,
}]),
..Default::default()
})
.await
.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
assert!(
switch_to_session(
&harness,
&lane,
&first_id,
cwd,
&state.chat_container,
&state,
&context,
&empty_catalog()
)
.await
);
let banner = strip_ansi(&state.chat_container.render(160).join("\n"));
assert!(
banner.contains("Skills (1)"),
"the banner must count the session's skill: {banner}"
);
assert!(
banner.contains("demo-skill"),
"the banner must name the session's skill: {banner}"
);
assert!(
!banner.contains("Skills (0)"),
"the banner must not fall back to the empty capability state: {banner}"
);
assert!(
!banner.contains("Tools (0) none"),
"the banner must not report zero tools: {banner}"
);
assert!(
banner.contains("Tools (1)") && banner.contains("probe-tool"),
"the banner must count the session's tools: {banner}"
);
}
#[tokio::test]
async fn new_session_creates_a_file_and_resets_footer() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
std::fs::create_dir_all(&root).unwrap();
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let previous_id = source.storage().metadata().id.clone();
source
.append_message(user_message_with_images(
"history that stays behind",
vec![],
))
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(source).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
state.footer.set_model("(prov) model");
state.footer.set_context_window(200_000);
state.footer.set_session_name(Some("old session"));
state.footer.add_usage(12_000, 3_400, 40_000, 1_200, 0.421);
state.footer.set_context_tokens(Some(126_400));
assert!(
start_new_session(
&harness,
&lane,
cwd,
&state.chat_container,
&state,
&context
)
.await
);
let new_id = harness.session().storage().metadata().id.clone();
assert_ne!(new_id, previous_id, "`/new` must swap the session id");
assert_eq!(
std::env::var(rpi_plugin_sdk::SESSION_ID_ENV).unwrap_or_default(),
new_id,
"the plugin env channel must follow the swap"
);
assert_eq!(
context.session_id().as_deref(),
Some(new_id.as_str()),
"the plugin tool context must follow the swap"
);
let files = jsonl_files_under(&root);
assert!(
files.iter().any(|path| path.contains(&new_id)),
"new session file missing under {}: {files:?}",
root.display()
);
assert_eq!(
files.iter().filter(|path| path.contains(&new_id)).count(),
1,
"exactly one file may back the new session: {files:?}"
);
assert!(
files.iter().any(|path| path.contains(&previous_id)),
"the previous session file must survive: {files:?}"
);
let stats = strip_ansi(&state.footer.render(120).join("\n"));
assert!(!stats.contains("↑12k"), "usage must reset: {stats}");
assert!(!stats.contains("$0.421"), "cost must reset: {stats}");
assert!(!stats.contains("old session"), "name must reset: {stats}");
assert!(
!stats.contains("63.2%"),
"context badge must reset: {stats}"
);
assert!(stats.contains("(prov) model"), "model kept: {stats}");
assert!(stats.contains("200k"), "window kept: {stats}");
let banner = strip_ansi(&state.chat_container.render(160).join("\n"));
assert!(
!banner.contains("history that stays behind"),
"the old transcript must not follow into the new session: {banner}"
);
assert!(
banner.contains(&format!("Started new session {new_id}.")),
"the note must name the new session: {banner}"
);
}
fn jsonl_files_under(root: &std::path::Path) -> Vec<String> {
let mut found = Vec::new();
let mut pending = vec![root.to_path_buf()];
while let Some(dir) = pending.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
pending.push(path);
} else if path.extension().is_some_and(|ext| ext == "jsonl") {
found.push(path.to_string_lossy().into_owned());
}
}
}
found
}
fn ephemeral_harness_session() -> rpi_harness::session::Session {
let storage = rpi_harness::session::memory::InMemorySessionStorage::new(
rpi_harness::session::types::SessionMetadata {
id: "ephemeral".into(),
created_at: 0,
parent_session_id: None,
},
Arc::new(rpi_harness::session::memory::SystemClock),
Arc::new(rpi_harness::session::session::DefaultIdGenerator::new()),
);
rpi_harness::session::Session::new(Arc::new(storage), None)
}
#[tokio::test]
async fn review_pr98_new_keeps_an_ephemeral_session_off_disk() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness
.set_session(ephemeral_harness_session())
.await
.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
assert!(!root.exists());
assert!(
start_new_session(
&harness,
&lane,
cwd,
&state.chat_container,
&state,
&context
)
.await
);
lane.append_message(user_message_with_images(
"review-only ephemeral message",
vec![],
))
.await
.unwrap();
let files = jsonl_files_under(cwd);
assert!(
files.is_empty(),
"--no-session must remain in memory after /new; persisted files: {files:?}"
);
}
#[tokio::test]
async fn review_pr98_new_must_not_switch_under_an_unscoped_user_bash_run() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let previous_id = source.storage().metadata().id.clone();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(source).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
let cancellation = state.begin_user_bash();
assert!(*state.status.lock().unwrap() == RunStatus::Idle);
let switched = start_new_session(
&harness,
&lane,
cwd,
&state.chat_container,
&state,
&context,
)
.await;
assert!(
!switched
|| cancellation.is_cancelled()
|| harness.session().storage().metadata().id == previous_id,
"/new switched while the previous session's unscoped !command remained active"
);
}
fn recorded_assistant_turn(
provider: &str,
model: &str,
usage: rpi_ai::types::Usage,
) -> AgentMessage {
let mut a = rpi_ai::AssistantMessage::empty(
rpi_ai::Api::Faux,
provider,
model,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or_default(),
);
a.stop_reason = rpi_ai::StopReason::Stop;
a.usage = usage;
AgentMessage::Assistant(Box::new(a))
}
#[tokio::test]
async fn switching_back_to_a_session_restores_its_model() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
std::fs::create_dir_all(&root).unwrap();
let first = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let first_id = first.storage().metadata().id.clone();
first
.append_message(recorded_assistant_turn(
"faux",
"second-model",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
let second = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let second_id = second.storage().metadata().id.clone();
assert_ne!(first_id, second_id);
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(second).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
lane.set_model(rpi_ai::Model::new(
"first-model",
"first",
rpi_ai::Api::Faux,
"faux",
"http://x",
))
.await
.unwrap();
let state = state_with_root(&root);
let context = rpi_extensions::ToolCallContext::new(cwd.to_string_lossy());
let catalog: Arc<std::sync::RwLock<Vec<rpi_ai::Model>>> =
Arc::new(std::sync::RwLock::new(vec![
rpi_ai::Model::new(
"first-model",
"first",
rpi_ai::Api::Faux,
"faux",
"http://x",
),
rpi_ai::Model::new(
"second-model",
"second",
rpi_ai::Api::Faux,
"faux",
"http://x",
),
]));
assert!(
switch_to_session(
&harness,
&lane,
&first_id,
cwd,
&state.chat_container,
&state,
&context,
&catalog
)
.await
);
let model = lane.get_model().await.unwrap();
assert_eq!(
model.id, "second-model",
"the lane must run the resumed session's own model"
);
let footer = strip_ansi(&state.footer.render(120).join("\n"));
assert!(
footer.contains("(faux) second-model"),
"the footer must show the resumed session's model: {footer}"
);
}
#[tokio::test]
async fn switching_back_to_a_session_restores_its_footer_usage() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
std::fs::create_dir_all(&root).unwrap();
let first = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let first_id = first.storage().metadata().id.clone();
first
.append_message(user_message_with_images("history prompt", vec![]))
.await
.unwrap();
first
.append_message(recorded_assistant_turn(
"faux",
"m",
rpi_ai::types::Usage {
input: 12_000,
output: 3_400,
cache_read: 40_000,
cache_write: 1_200,
..rpi_ai::types::Usage::zero()
},
))
.await
.unwrap();
first
.append_message(recorded_assistant_turn(
"faux",
"m",
rpi_ai::types::Usage {
input: 6_000,
output: 1_000,
cache_read: 20_000,
cache_write: 0,
total_tokens: 126_400,
..rpi_ai::types::Usage::zero()
},
))
.await
.unwrap();
let second = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let second_id = second.storage().metadata().id.clone();
assert_ne!(first_id, second_id);
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(second).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
state.footer.set_context_window(200_000);
state.footer.add_usage(111_111, 22_222, 0, 0, 9.999);
assert!(
switch_to_session(
&harness,
&lane,
&first_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&empty_catalog()
)
.await
);
let stats = strip_ansi(&state.footer.render(120).join("\n"));
assert!(stats.contains("↑18k"), "input totals restored: {stats}");
assert!(stats.contains("↓4.4k"), "output totals restored: {stats}");
assert!(
!stats.contains("↑111k"),
"stale totals must be gone: {stats}"
);
assert!(stats.contains("76.9%"), "cache hit rate restored: {stats}");
assert!(stats.contains("63.2%"), "context badge restored: {stats}");
}
#[tokio::test]
async fn unknown_session_model_leaves_the_current_model_in_place() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
std::fs::create_dir_all(&root).unwrap();
let first = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let first_id = first.storage().metadata().id.clone();
first
.append_message(recorded_assistant_turn(
"faux",
"retired-model",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
let second = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let second_id = second.storage().metadata().id.clone();
assert_ne!(first_id, second_id);
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(second).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let before = lane.get_model().await.unwrap();
let state = state_with_root(&root);
let catalog: Arc<std::sync::RwLock<Vec<rpi_ai::Model>>> =
Arc::new(std::sync::RwLock::new(vec![rpi_ai::Model::new(
"first-model",
"first",
rpi_ai::Api::Faux,
"faux",
"http://x",
)]));
assert!(
switch_to_session(
&harness,
&lane,
&first_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&catalog
)
.await,
"switching must succeed even when the recorded model is unavailable"
);
let after = lane.get_model().await.unwrap();
assert_eq!(after.id, before.id, "the lane model must stay untouched");
assert_eq!(
harness.session().storage().metadata().id,
first_id,
"the switch itself must have completed"
);
}
fn pr104_review_model(id: &str) -> rpi_ai::Model {
rpi_ai::Model::new(id, id, rpi_ai::Api::Faux, "faux", "http://x")
}
#[tokio::test]
async fn review_pr104_replay_preserves_the_live_cache_hit_rate() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let source_id = source.storage().metadata().id.clone();
let messages = [
recorded_assistant_turn(
"faux",
"m",
rpi_ai::types::Usage {
input: 100,
output: 1,
cache_read: 900,
..rpi_ai::types::Usage::zero()
},
),
recorded_assistant_turn(
"faux",
"m",
rpi_ai::types::Usage {
input: 900,
output: 1,
cache_read: 100,
..rpi_ai::types::Usage::zero()
},
),
];
for message in &messages {
source.append_message(message.clone()).await.unwrap();
}
let target = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(target).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let tui = Arc::new(TuiAltScreen::new(
Box::new(ProcessTerminal::new()),
true,
None,
));
for message in messages {
handle_agent_event(
AgentEvent::MessageEnd { message },
&tui,
&state,
&state.chat_container,
&lane,
)
.await;
}
let before = strip_ansi(&state.footer.render(120).join("\n"));
assert!(before.contains("CH10.0%"), "live hit rate: {before}");
assert!(
switch_to_session(
&harness,
&lane,
&source_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&empty_catalog(),
)
.await
);
let after = strip_ansi(&state.footer.render(120).join("\n"));
assert!(
after.contains("CH10.0%"),
"restoring the same two turns must preserve the live rate, not average all turns: {after}"
);
}
#[tokio::test]
async fn review_pr104_restore_model_must_follow_the_selected_branch() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let source_id = source.storage().metadata().id.clone();
let selected = source
.append_message(recorded_assistant_turn(
"faux",
"selected",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
source
.append_message(recorded_assistant_turn(
"faux",
"abandoned",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
source.move_lane("main", Some(&selected)).await.unwrap();
let target = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(target).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
lane.set_model(pr104_review_model("selected"))
.await
.unwrap();
let state = state_with_root(&root);
let catalog = Arc::new(std::sync::RwLock::new(vec![
pr104_review_model("selected"),
pr104_review_model("abandoned"),
]));
assert!(
switch_to_session(
&harness,
&lane,
&source_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&catalog,
)
.await
);
assert_eq!(
lane.get_tip_id().await.unwrap().as_deref(),
Some(selected.as_str())
);
assert_eq!(
lane.get_model().await.unwrap().id,
"selected",
"a later entry outside the selected branch must not select the model"
);
}
#[tokio::test]
async fn review_pr104_restore_model_must_ignore_a_child_lane() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let source_id = source.storage().metadata().id.clone();
let parent = source
.append_message(recorded_assistant_turn(
"faux",
"parent-model",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
source
.create_lane("child-review", Some(&parent))
.await
.unwrap();
source
.view("child-review")
.append_message(recorded_assistant_turn(
"faux",
"child-model",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
let target = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(target).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
lane.set_model(pr104_review_model("parent-model"))
.await
.unwrap();
let state = state_with_root(&root);
let catalog = Arc::new(std::sync::RwLock::new(vec![
pr104_review_model("parent-model"),
pr104_review_model("child-model"),
]));
assert!(
switch_to_session(
&harness,
&lane,
&source_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&catalog,
)
.await
);
assert_eq!(
lane.get_model().await.unwrap().id,
"parent-model",
"a child lane's later assistant turn must not replace the parent's model"
);
}
#[tokio::test]
async fn review_pr104_fork_preserves_a_model_pick_before_the_next_turn() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
source
.append_message(recorded_assistant_turn(
"faux",
"old-model",
rpi_ai::types::Usage::zero(),
))
.await
.unwrap();
let previous_id = source.storage().metadata().id.clone();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(source).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
apply_model_selection(&lane, &state, pr104_review_model("new-model"), false)
.await
.unwrap();
fork_session(
&harness,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
)
.await;
assert_ne!(harness.session().storage().metadata().id, previous_id);
assert_eq!(
lane.get_model().await.unwrap().id,
"new-model",
"/fork must not undo a successful /model selection made before the next assistant turn"
);
}
fn review_084_context_turn(model: &str, tokens: i64) -> AgentMessage {
recorded_assistant_turn(
"faux",
model,
rpi_ai::types::Usage {
input: tokens,
output: 1,
total_tokens: tokens,
..rpi_ai::types::Usage::zero()
},
)
}
#[tokio::test]
async fn rereview_pr104_context_must_not_come_from_a_child_lane() {
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let source_id = source.storage().metadata().id.clone();
let main_tip = source
.append_message(review_084_context_turn("parent-model", 50_000))
.await
.unwrap();
source
.create_lane("child-context", Some(&main_tip))
.await
.unwrap();
source
.view("child-context")
.append_message(review_084_context_turn("child-model", 190_000))
.await
.unwrap();
let target = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(target).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let mut parent_model = pr104_review_model("parent-model");
parent_model.context_window = 200_000;
let catalog = Arc::new(std::sync::RwLock::new(vec![parent_model]));
assert!(
switch_to_session(
&harness,
&lane,
&source_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&catalog,
)
.await
);
assert_eq!(lane.get_model().await.unwrap().id, "parent-model");
let footer = strip_ansi(&state.footer.render(160).join("\n"));
assert!(
footer.contains("25.0%/200k"),
"the main context is 50k/200k, not the child's 190k/200k: {footer}"
);
}
#[tokio::test]
async fn rereview_pr104_compaction_must_invalidate_pre_compaction_context() {
use rpi_harness::session::types::{ProvisionedEntry, ProvisionedKind};
let temporary = tempfile::tempdir().unwrap();
let cwd = temporary.path();
let root = cwd.join("sessions");
let source = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let source_id = source.storage().metadata().id.clone();
source
.append_message(review_084_context_turn("model", 100_000))
.await
.unwrap();
source
.append_entry(
ProvisionedEntry {
id: "review-context-compaction".into(),
kind: ProvisionedKind::Compaction {
summary: "Small replacement summary".into(),
retained_tail: vec![],
tokens_before: 100_000,
details: None,
usage: None,
},
},
"main",
)
.await
.unwrap();
let target = crate::session::create_jsonl_session(&root, &cwd.to_string_lossy())
.await
.unwrap();
let harness = AgentHarness::create(Default::default()).await.unwrap();
harness.set_session(target).await.unwrap();
let lane: Arc<dyn AgentLane> = harness.lane("main");
let state = state_with_root(&root);
let mut model = pr104_review_model("model");
model.context_window = 200_000;
let catalog = Arc::new(std::sync::RwLock::new(vec![model]));
assert!(
switch_to_session(
&harness,
&lane,
&source_id,
cwd,
&state.chat_container,
&state,
&rpi_extensions::ToolCallContext::new(cwd.to_string_lossy()),
&catalog,
)
.await
);
let footer = strip_ansi(&state.footer.render(160).join("\n"));
assert!(
footer.split_whitespace().any(|part| part == "0.0%/200k"),
"after compaction there is no new context report; old 50% must not reappear: {footer}"
);
}
struct BannerProbeTool {
schema: rpi_ai::types::Tool,
}
impl BannerProbeTool {
fn new(name: &str) -> Self {
Self {
schema: rpi_ai::types::Tool {
name: name.to_string(),
description: "probe tool".into(),
parameters: rpi_ai::types::Schema::new(serde_json::json!({
"type": "object",
"properties": {},
})),
constrained_sampling: None,
},
}
}
}
#[async_trait::async_trait]
impl rpi_agent::AgentTool for BannerProbeTool {
fn schema(&self) -> &rpi_ai::types::Tool {
&self.schema
}
fn label(&self) -> &str {
&self.schema.name
}
fn execution_mode(&self) -> rpi_agent::types::ToolExecutionMode {
rpi_agent::types::ToolExecutionMode::Parallel
}
async fn execute(
&self,
_tool_call_id: &str,
_params: serde_json::Value,
_signal: tokio_util::sync::CancellationToken,
_on_update: Arc<dyn Fn(rpi_agent::types::ToolResultPartial) + Send + Sync>,
) -> Result<rpi_agent::types::AgentToolResult, rpi_agent::AgentError> {
Ok(rpi_agent::types::AgentToolResult::text("probe"))
}
}