mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Remove core protocol dependency [2/2] (#20325)
## Why With the local model layer and app-server routing in place from PR1, this PR moves the active TUI runtime onto app-server notifications. The affected pieces share the same event flow, so the command surface, session state, bottom-pane prompts, chat rendering, history/status views, and tests move together to keep the stacked branch buildable. This PR also removes the obsolete compatibility surface that is no longer used after the migration. The proposed protocol-boundary verifier layer was dropped from the stack; enforcing that final boundary will be simpler once `codex-tui` no longer needs any `codex_protocol` references. This PR is part 2 of a 2-PR stack: 1. Add TUI-owned replacement models and extract app-server event routing. 2. Move the active TUI flow to app-server notifications and delete obsolete adapter code. ## What changed - Rewired app command and session handling to use app-server request and notification shapes. - Moved approval overlays, request-user-input flows, MCP elicitation, realtime events, and review commands onto the app-server-facing model surface. - Updated chat rendering, history cells, status views, multi-agent UI, replay state, and TUI tests to use app-server notifications plus the local models introduced in PR1. - Deleted `codex-rs/tui/src/app/app_server_adapter.rs` and the superseded `chatwidget/tests/background_events.rs` fixture path. ## Verification - `cargo check -p codex-tui --tests` - Top of stack: `cargo test -p codex-tui`
This commit is contained in:
committed by
GitHub
Unverified
parent
5cc5f12efc
commit
f2bc2f26a9
@@ -20,12 +20,8 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-short", ev);
|
||||
|
||||
let proposed_cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -89,9 +85,10 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
|
||||
async fn exec_approval_uses_approval_id_when_present() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
handle_exec_approval_request(
|
||||
&mut chat,
|
||||
"sub-short",
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: "call-parent".into(),
|
||||
approval_id: Some("approval-subcommand".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
@@ -105,21 +102,23 @@ async fn exec_approval_uses_approval_id_when_present() {
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
let mut found = false;
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::SubmitThreadOp {
|
||||
op: AppCommand::ExecApproval { id, decision, .. },
|
||||
op: Op::ExecApproval { id, decision, .. },
|
||||
..
|
||||
} = app_ev
|
||||
{
|
||||
assert_eq!(id, "approval-subcommand");
|
||||
assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved);
|
||||
assert_matches!(
|
||||
decision,
|
||||
codex_app_server_protocol::CommandExecutionApprovalDecision::Accept
|
||||
);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -146,12 +145,8 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-multi".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_multi),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
|
||||
let proposed_multi = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_multi.is_empty(),
|
||||
@@ -201,12 +196,8 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-long".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_long),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
|
||||
let proposed_long = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_long.is_empty(),
|
||||
@@ -356,28 +347,26 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
"-lc".to_string(),
|
||||
"echo orphaned".to_string(),
|
||||
];
|
||||
let parsed_cmd = codex_shell_command::parse_command::parse_command(&command);
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-orphan".to_string(),
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: "call-orphan".to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
let command_actions = codex_shell_command::parse_command::parse_command(&command)
|
||||
.into_iter()
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd))
|
||||
.collect();
|
||||
let cwd = chat.config.cwd.clone();
|
||||
handle_exec_end(
|
||||
&mut chat,
|
||||
AppServerThreadItem::CommandExecution {
|
||||
id: "call-orphan".to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
process_id: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
stdout: "done".to_string(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: "done".to_string(),
|
||||
exit_code: 0,
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: "done".to_string(),
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
});
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
command_actions,
|
||||
aggregated_output: Some("done".to_string()),
|
||||
exit_code: Some(0),
|
||||
duration_ms: Some(5),
|
||||
},
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");
|
||||
@@ -618,14 +607,7 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() {
|
||||
/*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-1".to_string(),
|
||||
msg: EventMsg::TerminalInteraction(TerminalInteractionEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
process_id: "proc-1".to_string(),
|
||||
stdin: "ls\n".to_string(),
|
||||
}),
|
||||
});
|
||||
terminal_interaction(&mut chat, "call-1", "proc-1", "ls\n");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -637,30 +619,13 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() {
|
||||
#[tokio::test]
|
||||
async fn unified_exec_wait_after_final_agent_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
begin_unified_exec_startup(&mut chat, "call-wait", "proc-1", "cargo test -p codex-core");
|
||||
terminal_interaction(&mut chat, "call-wait-stdin", "proc-1", "");
|
||||
|
||||
complete_assistant_message(&mut chat, "msg-1", "Final response.", /*phase*/ None);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Final response.".into()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -673,15 +638,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
begin_unified_exec_startup(
|
||||
&mut chat,
|
||||
@@ -691,22 +648,8 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
);
|
||||
terminal_interaction(&mut chat, "call-wait-stream-stdin", "proc-1", "");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
|
||||
delta: "Streaming response.".into(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_agent_message_delta(&mut chat, "Streaming response.");
|
||||
handle_turn_completed(&mut chat, "turn-wait-1", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -719,15 +662,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
let exec = begin_exec_with_source(
|
||||
&mut chat,
|
||||
@@ -743,16 +678,7 @@ async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {
|
||||
"Final response.",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Final response.".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: Some(125_000),
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", Some(125_000));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -777,11 +703,7 @@ async fn unified_exec_wait_status_header_updates_on_late_command_display() {
|
||||
recent_chunks: Vec::new(),
|
||||
});
|
||||
|
||||
chat.on_terminal_interaction(TerminalInteractionEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
process_id: "proc-1".to_string(),
|
||||
stdin: String::new(),
|
||||
});
|
||||
terminal_interaction(&mut chat, "call-1", "proc-1", "");
|
||||
|
||||
assert!(chat.active_cell.is_none());
|
||||
assert_eq!(
|
||||
@@ -815,16 +737,7 @@ async fn unified_exec_waiting_multiple_empty_snapshots() {
|
||||
assert_eq!(status.header(), "Waiting for background terminal");
|
||||
assert_eq!(status.details(), Some("just fix"));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-wait-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-wait-3", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -896,16 +809,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() {
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_active", active_combined);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-wait-3".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
let post_cells = drain_insert_history(&mut rx);
|
||||
let mut combined = pre_cells
|
||||
@@ -928,13 +832,7 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let image_path = chat.config.cwd.join("example.png");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-image".into(),
|
||||
msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
|
||||
call_id: "call-image".into(),
|
||||
path: image_path,
|
||||
}),
|
||||
});
|
||||
handle_view_image_tool_call(&mut chat, "call-image", image_path);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
@@ -946,16 +844,12 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
async fn image_generation_call_adds_history_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-image-generation".into(),
|
||||
msg: EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
|
||||
call_id: "call-image-generation".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("A tiny blue square".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
}),
|
||||
});
|
||||
handle_image_generation_end(
|
||||
&mut chat,
|
||||
"call-image-generation",
|
||||
Some("A tiny blue square".into()),
|
||||
Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
@@ -1049,9 +943,10 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = codex_protocol::protocol::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -1061,28 +956,17 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("!echo hi".to_string(), Vec::new(), Vec::new());
|
||||
@@ -1103,15 +987,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
async fn user_message_during_user_shell_command_is_queued_not_steered() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
let begin = begin_exec_with_source(
|
||||
&mut chat,
|
||||
"user-shell-sleep",
|
||||
@@ -1128,16 +1004,13 @@ async fn user_message_during_user_shell_command_is_queued_not_steered() {
|
||||
assert_eq!(chat.queued_user_message_texts(), vec!["hi".to_string()]);
|
||||
|
||||
end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("done".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-done",
|
||||
"done",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -1174,7 +1047,7 @@ async fn disabled_slash_command_while_task_running_snapshot() {
|
||||
//
|
||||
// Snapshot test: command approval modal
|
||||
//
|
||||
// Synthesizes a Codex ExecApprovalRequest event to trigger the approval modal
|
||||
// Synthesizes an exec approval request to trigger the approval modal
|
||||
// and snapshots the visual output using the ratatui TestBackend.
|
||||
#[tokio::test]
|
||||
async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
@@ -1184,7 +1057,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
// Inject an exec approval request to display the approval modal.
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-cmd".into(),
|
||||
@@ -1196,20 +1069,14 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
|
||||
"echo".into(),
|
||||
"hello".into(),
|
||||
"world".into(),
|
||||
])),
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment {
|
||||
command: vec!["echo".into(), "hello".into(), "world".into()],
|
||||
}),
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve", ev);
|
||||
// Render to a fixed-size test terminal and snapshot.
|
||||
// Call desired_height first and use that exact height for rendering.
|
||||
let width = 100;
|
||||
@@ -1247,7 +1114,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-cmd-noreason".into(),
|
||||
@@ -1257,20 +1124,14 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
|
||||
"echo".into(),
|
||||
"hello".into(),
|
||||
"world".into(),
|
||||
])),
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment {
|
||||
command: vec!["echo".into(), "hello".into(), "world".into()],
|
||||
}),
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-noreason".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve-noreason", ev);
|
||||
|
||||
let width = 100;
|
||||
let height = chat.desired_height(width);
|
||||
@@ -1297,7 +1158,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
|
||||
let script = "python - <<'PY'\nprint('hello')\nPY".to_string();
|
||||
let command = vec!["bash".into(), "-lc".into(), script];
|
||||
@@ -1309,16 +1170,12 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)),
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment { command }),
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-multiline-trunc".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve-multiline-trunc", ev);
|
||||
|
||||
let width = 100;
|
||||
let height = chat.desired_height(width);
|
||||
@@ -1345,7 +1202,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
|
||||
// Build a small changeset and a reason/grant_root to exercise the prompt text.
|
||||
let mut changes = HashMap::new();
|
||||
@@ -1362,10 +1219,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
reason: Some("The model wants to apply changes".into()),
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-patch".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "sub-approve-patch", ev);
|
||||
|
||||
// Render at the widget's desired height and snapshot.
|
||||
let height = chat.desired_height(/*width*/ 80);
|
||||
@@ -1390,15 +1244,7 @@ async fn interrupt_preserves_unified_exec_processes() {
|
||||
begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
@@ -1425,28 +1271,12 @@ async fn interrupt_preserves_unified_exec_processes() {
|
||||
async fn interrupt_preserves_unified_exec_wait_streak_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
let begin = begin_unified_exec_startup(&mut chat, "call-1", "process-1", "just fix");
|
||||
terminal_interaction(&mut chat, "call-1a", "process-1", "");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
@@ -1467,16 +1297,7 @@ async fn turn_complete_keeps_unified_exec_processes() {
|
||||
begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
@@ -1518,10 +1339,7 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "s1", ev);
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
"expected approval request to surface via modal without emitting history cells"
|
||||
@@ -1535,16 +1353,7 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
let begin = PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: true,
|
||||
changes: changes2,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(begin),
|
||||
});
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", changes2);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
let blob = lines_to_single_string(cells.last().unwrap());
|
||||
@@ -1561,19 +1370,13 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
let end = PatchApplyEndEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
stdout: "ok\n".into(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: CorePatchApplyStatus::Completed,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyEnd(end),
|
||||
});
|
||||
handle_patch_apply_end(
|
||||
&mut chat,
|
||||
"c1",
|
||||
"turn-c1",
|
||||
end_changes,
|
||||
AppServerPatchApplyStatus::Completed,
|
||||
);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
@@ -1592,16 +1395,17 @@ async fn apply_patch_manual_approval_adjusts_header() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"s1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let mut apply_changes = HashMap::new();
|
||||
@@ -1611,15 +1415,7 @@ async fn apply_patch_manual_approval_adjusts_header() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
});
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
@@ -1641,16 +1437,17 @@ async fn apply_patch_manual_flow_snapshot() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"s1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: Some("Manual review required".into()),
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
let history_before_apply = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
history_before_apply.is_empty(),
|
||||
@@ -1664,15 +1461,7 @@ async fn apply_patch_manual_flow_snapshot() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
});
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);
|
||||
let approved_lines = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("approved patch cell");
|
||||
@@ -1701,10 +1490,7 @@ async fn apply_patch_approval_sends_op_with_call_id() {
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-123".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "sub-123", ev);
|
||||
|
||||
// Approve via key press 'y'
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
@@ -1713,12 +1499,15 @@ async fn apply_patch_approval_sends_op_with_call_id() {
|
||||
let mut found = false;
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::SubmitThreadOp {
|
||||
op: AppCommand::PatchApproval { id, decision },
|
||||
op: Op::PatchApproval { id, decision },
|
||||
..
|
||||
} = app_ev
|
||||
{
|
||||
assert_eq!(id, "call-999");
|
||||
assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved);
|
||||
assert_matches!(
|
||||
decision,
|
||||
codex_app_server_protocol::FileChangeApprovalDecision::Accept
|
||||
);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -1736,23 +1525,24 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-xyz",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 2) User approves via 'y' and App receives a thread-scoped op
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
let mut maybe_op: Option<Op> = None;
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::SubmitThreadOp { op, .. } = app_ev {
|
||||
maybe_op = Some(op.into_core());
|
||||
maybe_op = Some(op);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1766,7 +1556,10 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
match forwarded {
|
||||
Op::PatchApproval { id, decision } => {
|
||||
assert_eq!(id, "call-1");
|
||||
assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved);
|
||||
assert_matches!(
|
||||
decision,
|
||||
codex_app_server_protocol::FileChangeApprovalDecision::Accept
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected op forwarded: {other:?}"),
|
||||
}
|
||||
@@ -1777,32 +1570,19 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
auto_approved: false,
|
||||
changes: changes2,
|
||||
}),
|
||||
});
|
||||
handle_patch_apply_begin(&mut chat, "call-1", "turn-call-1", changes2);
|
||||
let mut end_changes = HashMap::new();
|
||||
end_changes.insert(
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
stdout: String::from("ok"),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: CorePatchApplyStatus::Completed,
|
||||
}),
|
||||
});
|
||||
handle_patch_apply_end(
|
||||
&mut chat,
|
||||
"call-1",
|
||||
"turn-call-1",
|
||||
end_changes,
|
||||
AppServerPatchApplyStatus::Completed,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1812,7 +1592,7 @@ async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> {
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
|
||||
// Simulate a patch approval request from backend
|
||||
let mut changes = HashMap::new();
|
||||
@@ -1820,16 +1600,17 @@ async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> {
|
||||
PathBuf::from("a.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Render and ensure the approval modal title is present
|
||||
let area = Rect::new(0, 0, 80, 12);
|
||||
@@ -1863,7 +1644,7 @@ async fn apply_patch_request_omits_diff_summary_from_modal() -> anyhow::Result<(
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)?;
|
||||
.set(AskForApproval::OnRequest.to_core())?;
|
||||
|
||||
// Simulate backend asking to apply a patch adding two lines to README.md
|
||||
let mut changes = HashMap::new();
|
||||
@@ -1874,16 +1655,17 @@ async fn apply_patch_request_omits_diff_summary_from_modal() -> anyhow::Result<(
|
||||
content: "line one\nline two\n".into(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-apply".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-apply",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-apply".into(),
|
||||
turn_id: "turn-apply".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
|
||||
Reference in New Issue
Block a user