use std::path::Path;
use std::time::Duration;
use github_copilot_sdk::rpc::{
HistoryListRewindPointsResult, HistoryPreviewRewindRequest, HistoryRewindMode,
HistoryRewindOutcome, HistoryRewindRequest,
};
use super::support::assistant_message_content;
const FILE_NAME: &str = "rewind-sdk.txt";
const ORIGINAL_FILE_CONTENT: &str = "Original rewind content";
const PREPARED_FILE_CONTENT: &str = "Prepared rewind content";
const FILE_CONTENT: &str = "SDK rewind content";
#[tokio::test]
async fn should_restore_tracked_file_and_conversation() {
super::support::with_shared_e2e_context(
&E2E,
"rewind",
"should_restore_tracked_file_and_conversation",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let file_path = ctx.work_dir().join(FILE_NAME);
std::fs::write(&file_path, ORIGINAL_FILE_CONTENT).expect("write original file");
let client = ctx.start_client().await;
let session = client
.create_session(
ctx.approve_all_session_config()
.with_model("claude-sonnet-5")
.with_enable_file_change_tracking(true),
)
.await
.expect("create session");
let ready = session
.send_and_wait(format!(
"Use the edit tool to replace the exact contents of {FILE_NAME} from \
{ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. After the tool \
succeeds, reply with exactly SDK_REWIND_READY."
))
.await
.expect("send readiness turn")
.expect("readiness response");
assert_eq!(assistant_message_content(&ready), "SDK_REWIND_READY");
assert_eq!(
std::fs::read_to_string(&file_path).expect("read prepared file"),
PREPARED_FILE_CONTENT
);
let response = session
.send_and_wait(format!(
"Use the edit tool to replace the exact contents of {FILE_NAME} from \
{PREPARED_FILE_CONTENT} to {FILE_CONTENT}. After the tool succeeds, \
reply with exactly SDK_REWIND_DONE."
))
.await
.expect("send rewind setup prompt")
.expect("assistant message");
assert_eq!(assistant_message_content(&response), "SDK_REWIND_DONE");
assert_eq!(
std::fs::read_to_string(&file_path).expect("read tracked file"),
FILE_CONTENT
);
let rewind_points = wait_for_rewind_points(&session).await;
assert!(rewind_points.file_change_tracking_enabled);
assert_eq!(rewind_points.points.len(), 2);
let rewind_point = &rewind_points.points[1];
assert!(rewind_point.turn_changed_files);
assert!(rewind_point.can_restore_files);
assert_eq!(rewind_point.file_count, 1);
let preview = session
.rpc()
.history()
.preview_rewind(HistoryPreviewRewindRequest {
event_id: rewind_point.event_id.clone(),
})
.await
.expect("preview rewind");
assert!(preview.available);
assert_eq!(preview.files.len(), 1);
assert_same_path(&file_path, Path::new(&preview.files[0].path));
let rewind = session
.rpc()
.history()
.rewind(HistoryRewindRequest {
event_id: rewind_point.event_id.clone(),
mode: HistoryRewindMode::ConversationAndFiles,
})
.await
.expect("rewind conversation and files");
assert_eq!(rewind.outcome, HistoryRewindOutcome::Success);
assert!(rewind.events_removed.is_some_and(|count| count > 0));
assert_eq!(rewind.restored_files.len(), 1);
assert_same_path(&file_path, Path::new(&rewind.restored_files[0]));
assert_eq!(
std::fs::read_to_string(&file_path).expect("read restored file"),
PREPARED_FILE_CONTENT
);
let events = session.get_events().await.expect("get events after rewind");
assert!(events.iter().all(|event| event.id != rewind_point.event_id));
session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
async fn wait_for_rewind_points(
session: &github_copilot_sdk::session::Session,
) -> HistoryListRewindPointsResult {
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
loop {
let result = session
.rpc()
.history()
.list_rewind_points()
.await
.expect("list rewind points");
if result.unavailable_reason.is_none()
&& result.points.len() == 2
&& result.points[1].turn_changed_files
&& result.points[1].can_restore_files
&& result.points[1].file_count == 1
{
return result;
}
assert!(
tokio::time::Instant::now() < deadline,
"timed out waiting for a restorable rewind point: {result:?}"
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
fn assert_same_path(expected: &Path, actual: &Path) {
let expected = expected.to_string_lossy();
let actual = actual.to_string_lossy();
if cfg!(windows) {
let expected = expected.replace('\\', "/");
let actual = actual.replace('\\', "/");
assert!(
expected.eq_ignore_ascii_case(&actual),
"expected path {expected:?}, got {actual:?}"
);
} else {
assert_eq!(expected, actual);
}
}
static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard("rewind", 1);