mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
Merge branch 'code-branch-20251007-104544' into main
This commit is contained in:
@@ -19002,7 +19002,7 @@ fi\n\
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chatwidget::smoke_helpers::ChatWidgetHarness;
|
||||
use crate::history_cell::HistoryCellType;
|
||||
use crate::history_cell::{self, ExploreAggregationCell, HistoryCellType};
|
||||
use code_core::history::state::{
|
||||
AssistantStreamDelta,
|
||||
AssistantStreamState,
|
||||
@@ -19020,8 +19020,15 @@ mod tests {
|
||||
TextEmphasis,
|
||||
TextTone,
|
||||
};
|
||||
use code_core::parse_command::ParsedCommand;
|
||||
use code_core::protocol::OrderMeta;
|
||||
use code_core::protocol::{AgentMessageEvent, AgentStatusUpdateEvent, Event, EventMsg};
|
||||
use code_core::protocol::{
|
||||
AgentMessageEvent,
|
||||
AgentStatusUpdateEvent,
|
||||
Event,
|
||||
EventMsg,
|
||||
ExecCommandBeginEvent,
|
||||
};
|
||||
use code_core::protocol::AgentInfo as CoreAgentInfo;
|
||||
use ratatui::backend::TestBackend;
|
||||
use ratatui::text::Line;
|
||||
@@ -19086,6 +19093,69 @@ mod tests {
|
||||
let _ = chat.history_insert_plain_state_with_key(state, key, "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_explore_updates_even_with_stale_index() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
let chat = harness.chat();
|
||||
reset_history(chat);
|
||||
|
||||
let call_id = "call-explore".to_string();
|
||||
let order = OrderMeta {
|
||||
request_ordinal: 1,
|
||||
output_index: Some(0),
|
||||
sequence_number: Some(0),
|
||||
};
|
||||
|
||||
chat.handle_code_event(Event {
|
||||
id: call_id.clone(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
command: vec!["bash".into(), "-lc".into(), "cat foo.txt".into()],
|
||||
cwd: std::env::temp_dir(),
|
||||
parsed_cmd: vec![ParsedCommand::Read {
|
||||
cmd: "cat foo.txt".to_string(),
|
||||
name: "foo.txt".to_string(),
|
||||
}],
|
||||
}),
|
||||
order: Some(order.clone()),
|
||||
});
|
||||
|
||||
let exec_call_id = ExecCallId(call_id.clone());
|
||||
let running = chat
|
||||
.exec
|
||||
.running_commands
|
||||
.get_mut(&exec_call_id)
|
||||
.expect("explore command should be tracked");
|
||||
let (agg_idx, entry_idx) = running
|
||||
.explore_entry
|
||||
.expect("read command should register an explore entry");
|
||||
|
||||
// Simulate an out-of-date index so finalize must recover by searching.
|
||||
running.explore_entry = Some((usize::MAX, entry_idx));
|
||||
chat.exec.running_explore_agg_index = Some(usize::MAX);
|
||||
|
||||
chat.finalize_all_running_due_to_answer();
|
||||
|
||||
let cell = chat.history_cells[agg_idx]
|
||||
.as_any()
|
||||
.downcast_ref::<ExploreAggregationCell>()
|
||||
.expect("explore aggregation cell should remain present");
|
||||
let entry = cell
|
||||
.record()
|
||||
.entries
|
||||
.get(entry_idx)
|
||||
.expect("entry index should still be valid");
|
||||
assert!(
|
||||
matches!(entry.status, history_cell::ExploreEntryStatus::Success),
|
||||
"explore entry should be marked successful instead of remaining running"
|
||||
);
|
||||
assert!(
|
||||
!chat.exec.running_commands.contains_key(&exec_call_id),
|
||||
"finalization should clear the running command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordering_keeps_new_answers_after_prior_backgrounds() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
|
||||
@@ -56,6 +56,74 @@ fn find_trailing_explore_agg(chat: &ChatWidget<'_>) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn update_explore_entry_status(
|
||||
chat: &mut ChatWidget<'_>,
|
||||
preferred_index: Option<usize>,
|
||||
entry_idx: usize,
|
||||
status: history_cell::ExploreEntryStatus,
|
||||
) -> Option<usize> {
|
||||
fn try_update_at(
|
||||
chat: &mut ChatWidget<'_>,
|
||||
idx: usize,
|
||||
entry_idx: usize,
|
||||
status: &history_cell::ExploreEntryStatus,
|
||||
) -> Option<usize> {
|
||||
if idx >= chat.history_cells.len() {
|
||||
return None;
|
||||
}
|
||||
let cell = chat.history_cells[idx]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()?;
|
||||
if entry_idx >= cell.record().entries.len() {
|
||||
return None;
|
||||
}
|
||||
let mut record = cell.record().clone();
|
||||
history_cell::explore_record_update_status(&mut record, entry_idx, status.clone());
|
||||
let replacement = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
chat.history_replace_with_record(
|
||||
idx,
|
||||
Box::new(replacement),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
Some(idx)
|
||||
}
|
||||
|
||||
if let Some(idx) = preferred_index.and_then(|i| try_update_at(chat, i, entry_idx, &status)) {
|
||||
return Some(idx);
|
||||
}
|
||||
if let Some(idx) = chat
|
||||
.exec
|
||||
.running_explore_agg_index
|
||||
.and_then(|i| try_update_at(chat, i, entry_idx, &status))
|
||||
{
|
||||
return Some(idx);
|
||||
}
|
||||
for i in (0..chat.history_cells.len()).rev() {
|
||||
let Some(cell) = chat.history_cells[i]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if entry_idx >= cell.record().entries.len() {
|
||||
continue;
|
||||
}
|
||||
if !matches!(
|
||||
cell.record().entries[entry_idx].status,
|
||||
history_cell::ExploreEntryStatus::Running
|
||||
| history_cell::ExploreEntryStatus::Error { .. }
|
||||
| history_cell::ExploreEntryStatus::NotFound
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if let Some(idx) = try_update_at(chat, i, entry_idx, &status) {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn exec_record_from_begin(ev: &ExecCommandBeginEvent) -> ExecRecord {
|
||||
let action = history_cell::action_enum_from_parsed(&ev.parsed_cmd);
|
||||
ExecRecord {
|
||||
@@ -344,27 +412,14 @@ pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
|
||||
}
|
||||
|
||||
if let Some((agg_idx, entry_idx)) = explore_entry {
|
||||
if *agg_idx < chat.history_cells.len() {
|
||||
if let Some(existing) = chat.history_cells[*agg_idx]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()
|
||||
{
|
||||
let mut record = existing.record().clone();
|
||||
history_cell::explore_record_update_status(
|
||||
&mut record,
|
||||
*entry_idx,
|
||||
history_cell::ExploreEntryStatus::Success,
|
||||
);
|
||||
let cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
chat.history_replace_with_record(
|
||||
*agg_idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
agg_was_updated = true;
|
||||
}
|
||||
}
|
||||
let updated = update_explore_entry_status(
|
||||
chat,
|
||||
Some(*agg_idx),
|
||||
*entry_idx,
|
||||
history_cell::ExploreEntryStatus::Success,
|
||||
)
|
||||
.is_some();
|
||||
agg_was_updated |= updated;
|
||||
}
|
||||
|
||||
remove_after_finalize.push(call_id.clone());
|
||||
@@ -748,14 +803,19 @@ pub(super) fn handle_exec_end_now(
|
||||
order: &OrderMeta,
|
||||
) {
|
||||
let call_id = super::ExecCallId(ev.call_id.clone());
|
||||
if chat.exec.should_suppress_exec_end(&call_id) {
|
||||
let suppressing = chat.exec.should_suppress_exec_end(&call_id);
|
||||
if suppressing {
|
||||
chat.exec.unsuppress_exec_end(&call_id);
|
||||
chat.ended_call_ids.insert(call_id);
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
if !chat.exec.running_commands.contains_key(&call_id) {
|
||||
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
|
||||
chat.maybe_hide_spinner();
|
||||
chat.refresh_auto_drive_visuals();
|
||||
return;
|
||||
}
|
||||
}
|
||||
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
|
||||
chat
|
||||
.ended_call_ids
|
||||
.insert(super::ExecCallId(ev.call_id.clone()));
|
||||
// If this call was already marked as cancelled, drop the End to avoid
|
||||
// inserting a duplicate completed cell after the user interrupt.
|
||||
if chat
|
||||
@@ -836,20 +896,7 @@ pub(super) fn handle_exec_end_now(
|
||||
exit_code: Some(exit_code),
|
||||
},
|
||||
};
|
||||
if let Some(mut record) = chat.history_cells.get(agg_idx).and_then(|cell| {
|
||||
cell.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()
|
||||
.map(|existing| existing.record().clone())
|
||||
}) {
|
||||
history_cell::explore_record_update_status(&mut record, entry_idx, status.clone());
|
||||
let cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
chat.history_replace_with_record(
|
||||
agg_idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
}
|
||||
let updated_index = update_explore_entry_status(chat, Some(agg_idx), entry_idx, status.clone());
|
||||
if !chat
|
||||
.exec
|
||||
.running_commands
|
||||
@@ -857,6 +904,8 @@ pub(super) fn handle_exec_end_now(
|
||||
.any(|rc| rc.explore_entry.is_some())
|
||||
{
|
||||
chat.exec.running_explore_agg_index = None;
|
||||
} else if let Some(actual_idx) = updated_index {
|
||||
chat.exec.running_explore_agg_index = Some(actual_idx);
|
||||
}
|
||||
let status_text = match status {
|
||||
history_cell::ExploreEntryStatus::Success => match action {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
use code_core::protocol::{
|
||||
Event,
|
||||
EventMsg,
|
||||
ExecCommandBeginEvent,
|
||||
ExecCommandEndEvent,
|
||||
OrderMeta,
|
||||
PatchApplyBeginEvent,
|
||||
PatchApplyEndEvent,
|
||||
};
|
||||
use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
fn next_order_meta(request_ordinal: u64, seq: &mut u64) -> OrderMeta {
|
||||
let order = OrderMeta {
|
||||
request_ordinal,
|
||||
output_index: Some(0),
|
||||
sequence_number: Some(*seq),
|
||||
};
|
||||
*seq += 1;
|
||||
order
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_cell_clears_after_patch_flow() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
let mut seq = 0_u64;
|
||||
let call_id = "call_bug";
|
||||
let cwd = PathBuf::from("/tmp");
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "exec-begin".to_string(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
command: vec!["bash".into(), "-lc".into(), "apply_patch".into()],
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: Vec::new(),
|
||||
}),
|
||||
order: Some(next_order_meta(1, &mut seq)),
|
||||
});
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "patch-begin".to_string(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
auto_approved: true,
|
||||
changes: HashMap::new(),
|
||||
}),
|
||||
order: Some(next_order_meta(1, &mut seq)),
|
||||
});
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "exec-end".to_string(),
|
||||
event_seq: 1,
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
exit_code: 0,
|
||||
duration: Duration::from_millis(50),
|
||||
}),
|
||||
order: Some(next_order_meta(1, &mut seq)),
|
||||
});
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "patch-end".to_string(),
|
||||
event_seq: 1,
|
||||
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
stdout: "Success".into(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
}),
|
||||
order: Some(next_order_meta(1, &mut seq)),
|
||||
});
|
||||
|
||||
let output = render_chat_widget_to_vt100(&mut harness, 80, 12);
|
||||
assert!(
|
||||
!output.contains("Running"),
|
||||
"exec cell should not remain running after patch apply:\n{}",
|
||||
output
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user