mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
V2
This commit is contained in:
@@ -562,6 +562,109 @@ mod tests {
|
||||
out
|
||||
}
|
||||
|
||||
fn normalize_patch_for_test(input: &str, root: &Path) -> String {
|
||||
let root_str = root.display().to_string().replace('\\', "/");
|
||||
let mut replaced = input.replace(&root_str, "<TMP>");
|
||||
if !replaced.ends_with('\n') {
|
||||
replaced.push('\n');
|
||||
}
|
||||
replaced
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo_patch_returns_none_without_baseline() {
|
||||
let mut tracker = TurnDiffTracker::new();
|
||||
assert_eq!(tracker.build_undo_patch().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo_patch_restores_updated_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("undo.txt");
|
||||
fs::write(&path, "before\n").unwrap();
|
||||
|
||||
let mut tracker = TurnDiffTracker::new();
|
||||
let update_changes = HashMap::from([(
|
||||
path.clone(),
|
||||
FileChange::Update {
|
||||
unified_diff: String::new(),
|
||||
move_path: None,
|
||||
},
|
||||
)]);
|
||||
tracker.on_patch_begin(&update_changes);
|
||||
|
||||
fs::write(&path, "after\n").unwrap();
|
||||
|
||||
let patch = tracker
|
||||
.build_undo_patch()
|
||||
.expect("undo patch")
|
||||
.expect("some undo patch");
|
||||
let normalized = normalize_patch_for_test(&patch, dir.path());
|
||||
let expected = concat!(
|
||||
"*** Begin Patch\n",
|
||||
"*** Delete File: <TMP>/undo.txt\n",
|
||||
"*** Add File: <TMP>/undo.txt\n",
|
||||
"+before\n",
|
||||
"*** End Patch\n",
|
||||
);
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo_patch_restores_deleted_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("gone.txt");
|
||||
fs::write(&path, "gone\n").unwrap();
|
||||
|
||||
let mut tracker = TurnDiffTracker::new();
|
||||
let delete_changes = HashMap::from([(
|
||||
path.clone(),
|
||||
FileChange::Delete {
|
||||
content: "gone\n".to_string(),
|
||||
},
|
||||
)]);
|
||||
tracker.on_patch_begin(&delete_changes);
|
||||
|
||||
fs::remove_file(&path).unwrap();
|
||||
|
||||
let patch = tracker
|
||||
.build_undo_patch()
|
||||
.expect("undo patch")
|
||||
.expect("some undo patch");
|
||||
let normalized = normalize_patch_for_test(&patch, dir.path());
|
||||
let expected = concat!(
|
||||
"*** Begin Patch\n",
|
||||
"*** Add File: <TMP>/gone.txt\n",
|
||||
"+gone\n",
|
||||
"*** End Patch\n",
|
||||
);
|
||||
assert_eq!(normalized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_undo_patch_rejects_non_utf8_content() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("binary.bin");
|
||||
fs::write(&path, [0xff, 0xfe, 0x00]).unwrap();
|
||||
|
||||
let mut tracker = TurnDiffTracker::new();
|
||||
let update_changes = HashMap::from([(
|
||||
path.clone(),
|
||||
FileChange::Update {
|
||||
unified_diff: String::new(),
|
||||
move_path: None,
|
||||
},
|
||||
)]);
|
||||
tracker.on_patch_begin(&update_changes);
|
||||
|
||||
let err = tracker.build_undo_patch().unwrap_err();
|
||||
let message = format!("{err:#}");
|
||||
assert!(
|
||||
message.contains("undo is not supported for non-UTF8 baseline file"),
|
||||
"unexpected error message: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accumulates_add_and_update() {
|
||||
let mut acc = TurnDiffTracker::new();
|
||||
|
||||
@@ -411,6 +411,8 @@ impl ChatWidget {
|
||||
|
||||
fn on_background_event(&mut self, message: String) {
|
||||
debug!("BackgroundEvent: {message}");
|
||||
self.add_to_history(history_cell::new_background_event(message));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_stream_error(&mut self, message: String) {
|
||||
@@ -863,8 +865,7 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
SlashCommand::Undo => {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(Op::UndoLastTurnDiff));
|
||||
self.open_undo_confirmation_popup();
|
||||
}
|
||||
SlashCommand::Mention => {
|
||||
self.insert_str("@");
|
||||
@@ -1257,6 +1258,42 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
|
||||
fn open_undo_confirmation_popup(&mut self) {
|
||||
let confirm_message = "Undoing the last Codex turn diff.".to_string();
|
||||
let undo_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_background_event(confirm_message.clone()),
|
||||
)));
|
||||
tx.send(AppEvent::CodexOp(Op::UndoLastTurnDiff));
|
||||
})];
|
||||
|
||||
let mut items = Vec::new();
|
||||
items.push(SelectionItem {
|
||||
name: "Undo last turn diff".to_string(),
|
||||
description: Some(
|
||||
"Revert files that Codex changed during the most recent turn.".to_string(),
|
||||
),
|
||||
is_current: false,
|
||||
actions: undo_actions,
|
||||
});
|
||||
items.push(SelectionItem {
|
||||
name: "Cancel".to_string(),
|
||||
description: Some("Close without undoing any files.".to_string()),
|
||||
is_current: false,
|
||||
actions: Vec::new(),
|
||||
});
|
||||
|
||||
self.bottom_pane.show_selection_view(
|
||||
"Undo last Codex turn?".to_string(),
|
||||
Some(
|
||||
"Codex will apply a patch to restore files from before the previous turn."
|
||||
.to_string(),
|
||||
),
|
||||
Some("Press Enter to confirm or Esc to cancel".to_string()),
|
||||
items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the approval policy in the widget's config copy.
|
||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||
self.config.approval_policy = policy;
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_core::protocol::AgentMessageEvent;
|
||||
use codex_core::protocol::AgentReasoningDeltaEvent;
|
||||
use codex_core::protocol::AgentReasoningEvent;
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::BackgroundEventEvent;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::ExecApprovalRequestEvent;
|
||||
@@ -614,6 +615,58 @@ fn disabled_slash_command_while_task_running_snapshot() {
|
||||
assert_snapshot!(blob);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_command_requires_confirmation() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.dispatch_command(SlashCommand::Undo);
|
||||
|
||||
assert!(rx.try_recv().is_err(), "undo should require confirmation");
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
let mut undo_requested = false;
|
||||
let mut history_lines = Vec::new();
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
history_lines.push(cell.display_lines(80));
|
||||
}
|
||||
AppEvent::CodexOp(Op::UndoLastTurnDiff) => {
|
||||
undo_requested = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(undo_requested, "expected undo op after confirmation");
|
||||
|
||||
let combined = history_lines
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert!(combined.contains("Undoing the last Codex turn diff."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_events_are_rendered_in_history() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "undo".to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
message: "Reverted last turn diff.".to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
let history = drain_insert_history(&mut rx);
|
||||
let combined = history
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert!(combined.contains("Reverted last turn diff."));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn binary_size_transcript_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
@@ -1064,6 +1064,11 @@ pub(crate) fn new_stream_error_event(message: String) -> PlainHistoryCell {
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_background_event(message: String) -> PlainHistoryCell {
|
||||
let lines: Vec<Line<'static>> = vec![vec![padded_emoji("ℹ️").into(), message.into()].into()];
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
/// Render a user‑friendly plan update styled like a checkbox todo list.
|
||||
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell {
|
||||
let UpdatePlanArgs { explanation, plan } = update;
|
||||
|
||||
Reference in New Issue
Block a user