Skip to content

Commit 4e14533

Browse files
authored
Merge branch 'main' into dependabot/cargo/lru-0.18.3
2 parents c8ffa60 + 16ce3c6 commit 4e14533

11 files changed

Lines changed: 185 additions & 123 deletions

File tree

crates/tui/src/plugins/marketplace/document.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,17 @@ mod tests {
208208
assert!(!valid_marketplace_name("a".repeat(65).as_str()));
209209
}
210210

211+
#[cfg(unix)]
211212
#[test]
212213
fn load_refuses_symlink_documents() {
213214
let dir = tempfile::tempdir().unwrap();
214215
let real = dir.path().join("real.json");
215216
std::fs::write(&real, "{}").unwrap();
216217
let link = dir.path().join("link.json");
217-
#[cfg(unix)]
218218
std::os::unix::fs::symlink(&real, &link).unwrap();
219-
#[cfg(not(unix))]
220-
let link = real.clone();
221219

222220
let error = load_catalog_document("test", dir.path(), link.to_str().unwrap())
223221
.expect_err("symlink document must be refused");
224-
#[cfg(unix)]
225222
assert!(error.contains("symlink"), "{error}");
226223
}
227224

crates/tui/src/runtime_api/tests.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11191,6 +11191,7 @@ async fn marketplace_catalog_lifecycle_over_http_lists_installs_and_removes() ->
1119111191
Ok(())
1119211192
}
1119311193

11194+
#[cfg(unix)]
1119411195
#[tokio::test]
1119511196
async fn marketplace_add_rejects_symlink_documents_over_http() -> Result<()> {
1119611197
let tmp = tempfile::tempdir()?;
@@ -11203,10 +11204,7 @@ async fn marketplace_add_rejects_symlink_documents_over_http() -> Result<()> {
1120311204
let real = catalog_dir.join("real.json");
1120411205
fs::write(&real, r#"{"plugins":[]}"#)?;
1120511206
let link = catalog_dir.join("link.json");
11206-
#[cfg(unix)]
1120711207
std::os::unix::fs::symlink(&real, &link)?;
11208-
#[cfg(not(unix))]
11209-
let link = real;
1121011208

1121111209
let Some((addr, handle)) = spawn_plugin_api_server(root, workspace).await? else {
1121211210
return Ok(());
@@ -11221,10 +11219,7 @@ async fn marketplace_add_rejects_symlink_documents_over_http() -> Result<()> {
1122111219
}))
1122211220
.send()
1122311221
.await?;
11224-
#[cfg(unix)]
1122511222
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
11226-
#[cfg(not(unix))]
11227-
assert!(resp.status().is_success());
1122811223

1122911224
handle.abort();
1123011225
Ok(())

crates/tui/src/runtime_threads/tests.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3719,24 +3719,30 @@ async fn wait_for_terminal_turn(
37193719
| RuntimeTurnStatus::Interrupted
37203720
| RuntimeTurnStatus::Canceled
37213721
);
3722-
if terminal {
3722+
let timed_out = Instant::now() >= deadline;
3723+
if terminal || timed_out {
37233724
let receipt_is_durable =
37243725
manager
37253726
.events_since(&turn.thread_id, None)?
37263727
.iter()
37273728
.any(|event| {
37283729
event.event == "turn.completed" && event.turn_id.as_deref() == Some(turn_id)
37293730
});
3730-
let claim_is_clear = manager
3731+
let active_claim_present = manager
37313732
.active_turn_flags(&turn.thread_id, turn_id)
37323733
.await
3733-
.is_none();
3734-
if receipt_is_durable && claim_is_clear {
3734+
.is_some();
3735+
if terminal && receipt_is_durable && !active_claim_present {
37353736
return Ok(turn);
37363737
}
3737-
}
3738-
if Instant::now() >= deadline {
3739-
bail!("Timed out waiting for turn {turn_id}");
3738+
if timed_out {
3739+
bail!(
3740+
"Timed out waiting for turn {turn_id}: status={:?}, \
3741+
completion_receipt_present={receipt_is_durable}, \
3742+
active_claim_present={active_claim_present}",
3743+
turn.status
3744+
);
3745+
}
37403746
}
37413747
sleep(Duration::from_millis(20)).await;
37423748
}
@@ -5420,7 +5426,8 @@ async fn monitor_separates_lifecycle_start_from_billing_dispatch_and_child_usage
54205426
})
54215427
.await?;
54225428

5423-
let completed = wait_for_terminal_turn(&manager, &turn.id, Duration::from_secs(2)).await?;
5429+
let completed =
5430+
wait_for_terminal_turn(&manager, &turn.id, TURN_SETTLEMENT_DEADLOCK_TIMEOUT).await?;
54245431
assert_eq!(completed.effective_provider.as_deref(), Some("stepfun"));
54255432
assert_eq!(
54265433
completed.effective_provider_id.as_deref(),

crates/tui/src/tui/app/init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ impl App {
722722
selected_attachment_index: None,
723723
slash_menu_selected: 0,
724724
slash_menu_hidden: false,
725-
mention_menu_selected: 0,
725+
mention_menu_selected: 0,
726726
mention_menu_hidden: false,
727727
mention_completion_cache: None,
728728
mention_discovery: crate::tui::mention_completion::MentionDiscovery::default(),

crates/tui/src/tui/mouse_ui.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,6 @@ fn handle_plugin_cta_mouse(app: &mut App, mouse: MouseEvent) -> Option<Vec<ViewE
337337
Some(Vec::new())
338338
}
339339

340-
/// Handle mouse events within the composer area.
341-
/// Returns true if the event was consumed.
342-
343340
/// Slash-autocomplete rows painted inside the composer. Click selects
344341
/// (second click on the same row applies, matching the command palette);
345342
/// wheel moves the highlight. Returns true when the event was consumed so
@@ -349,12 +346,13 @@ fn handle_slash_autocomplete_mouse(app: &mut App, mouse: MouseEvent) -> bool {
349346
if hitboxes.is_empty() {
350347
return false;
351348
}
352-
let over_row = hitboxes.iter().find_map(|(idx, rect)| {
353-
mouse_hits_rect(mouse, Some(*rect)).then_some(*idx)
354-
});
349+
let over_row = hitboxes
350+
.iter()
351+
.find_map(|(idx, rect)| mouse_hits_rect(mouse, Some(*rect)).then_some(*idx));
355352
let over_menu = over_row.is_some()
356353
|| hitboxes.iter().any(|(_, rect)| {
357-
mouse.row >= rect.y && mouse.row < rect.y.saturating_add(rect.height)
354+
mouse.row >= rect.y
355+
&& mouse.row < rect.y.saturating_add(rect.height)
358356
&& mouse.column >= rect.x
359357
&& mouse.column < rect.x.saturating_add(rect.width)
360358
});
@@ -405,6 +403,8 @@ fn handle_slash_autocomplete_mouse(app: &mut App, mouse: MouseEvent) -> bool {
405403
}
406404
}
407405

406+
/// Handle mouse events within the composer area.
407+
/// Returns true if the event was consumed.
408408
pub(crate) fn handle_composer_mouse(app: &mut App, mouse: MouseEvent) -> bool {
409409
// Use outer area for hit-testing (includes border).
410410
let Some(area) = app.viewport.last_composer_area else {
@@ -1973,8 +1973,6 @@ mod tests {
19731973
crate::tui::hover_layer::clear_pointer();
19741974
}
19751975

1976-
1977-
19781976
#[test]
19791977
fn slash_autocomplete_click_selects_and_second_click_applies() {
19801978
let mut app = create_test_app();
@@ -1986,18 +1984,22 @@ mod tests {
19861984
app.slash_menu_selected = 0;
19871985
// Simulate two painted rows from ComposerWidget.
19881986
app.viewport.last_composer_area = Some(Rect::new(0, 18, 80, 6));
1989-
*app.viewport.last_slash_menu_hitboxes.borrow_mut() = vec![
1990-
(0, Rect::new(1, 20, 78, 1)),
1991-
(1, Rect::new(1, 21, 78, 1)),
1992-
];
1987+
*app.viewport.last_slash_menu_hitboxes.borrow_mut() =
1988+
vec![(0, Rect::new(1, 20, 78, 1)), (1, Rect::new(1, 21, 78, 1))];
19931989

19941990
assert!(
19951991
handle_composer_mouse(&mut app, left_click(5, 21)),
19961992
"slash row click must be consumed by the composer"
19971993
);
1998-
assert_eq!(app.slash_menu_selected, 1, "click on another row highlights it");
1994+
assert_eq!(
1995+
app.slash_menu_selected, 1,
1996+
"click on another row highlights it"
1997+
);
19991998
let before = app.input.clone();
2000-
assert_eq!(before, "/he", "select-only click must not rewrite the composer");
1999+
assert_eq!(
2000+
before, "/he",
2001+
"select-only click must not rewrite the composer"
2002+
);
20012003

20022004
assert!(handle_composer_mouse(&mut app, left_click(5, 21)));
20032005
assert_ne!(app.input, before, "click on the highlighted row applies it");
@@ -2018,10 +2020,8 @@ mod tests {
20182020
app.slash_menu_hidden = false;
20192021
app.slash_menu_selected = 0;
20202022
app.viewport.last_composer_area = Some(Rect::new(0, 18, 80, 6));
2021-
*app.viewport.last_slash_menu_hitboxes.borrow_mut() = vec![
2022-
(0, Rect::new(1, 20, 78, 1)),
2023-
(1, Rect::new(1, 21, 78, 1)),
2024-
];
2023+
*app.viewport.last_slash_menu_hitboxes.borrow_mut() =
2024+
vec![(0, Rect::new(1, 20, 78, 1)), (1, Rect::new(1, 21, 78, 1))];
20252025
let entries = crate::tui::slash_menu::visible_slash_menu_entries(&app, 128);
20262026
assert!(entries.len() >= 2, "prefix must offer multiple entries");
20272027

@@ -2048,7 +2048,6 @@ mod tests {
20482048
assert_eq!(app.slash_menu_selected, 0);
20492049
}
20502050

2051-
20522051
#[test]
20532052
fn send_click_matches_the_keyboard_submit_and_focus_never_leaves_the_composer() {
20542053
let mut app = create_test_app();

crates/tui/src/tui/ui/tests.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4927,7 +4927,10 @@ fn paste_burst_does_not_leak_into_composer_while_a_modal_owns_keys() {
49274927
&mut app,
49284928
now + crate::tui::paste_burst::PasteBurst::recommended_flush_delay(),
49294929
);
4930-
assert!(!flushed, "modal-owned keys must not flush into the composer");
4930+
assert!(
4931+
!flushed,
4932+
"modal-owned keys must not flush into the composer"
4933+
);
49314934
assert!(
49324935
app.input.is_empty() || app.input == "/",
49334936
"composer must not gain leaked burst text under a modal: {:?}",

crates/tui/src/tui/widgets/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,7 +1359,11 @@ impl Renderable for ComposerWidget<'_> {
13591359
fn render(&self, area: Rect, buf: &mut Buffer) {
13601360
// Slash rows are re-recorded below; clear first so a closed or
13611361
// resized menu cannot keep stale hitboxes from the prior frame.
1362-
self.app.viewport.last_slash_menu_hitboxes.borrow_mut().clear();
1362+
self.app
1363+
.viewport
1364+
.last_slash_menu_hitboxes
1365+
.borrow_mut()
1366+
.clear();
13631367
let background = Style::default().bg(self.app.ui_theme.composer_bg);
13641368
let has_panel = self.has_panel(area);
13651369
let inner_area = self.inner_area(area);
@@ -1826,10 +1830,7 @@ impl Renderable for ComposerWidget<'_> {
18261830
.viewport
18271831
.last_slash_menu_hitboxes
18281832
.borrow_mut()
1829-
.push((
1830-
idx,
1831-
Rect::new(inner_area.x, row_y, inner_area.width, 1),
1832-
));
1833+
.push((idx, Rect::new(inner_area.x, row_y, inner_area.width, 1)));
18331834
}
18341835

18351836
if name_was_truncated || description_was_truncated {

crates/tui/tests/cucumber/active_composer_pointer_pty.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,13 @@ fn assert_startup_contract(frame: &Frame, rows: u16, cols: u16, size: &str) {
341341

342342
fn assert_live_shell_contract(frame: &Frame, cols: u16, size: &str) {
343343
let text = frame.text();
344+
// The bottom metrics row owns the model; repository state belongs to
345+
// the launch header and git view. This sealed offline session uses the
346+
// default model, which must remain visible even at 40 columns.
347+
let metrics = frame.row(frame.rows().saturating_sub(1));
344348
assert!(
345-
text.contains("no git"),
346-
"{size}: live shell misses the info line\n{}",
349+
metrics.contains("deepseek-v4-pro"),
350+
"{size}: live shell misses the model in the metrics line\n{}",
347351
frame.debug_dump()
348352
);
349353
// The shell advertises one help route per surface: the info line's

crates/tui/tests/cucumber/plugin_e2e_acceptance.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -693,19 +693,22 @@ fn review_confirmation_in_text(text: &str) -> Option<String> {
693693
// The confirmation is `/plugin trust demo <64-hex>.<64-hex>` (129 chars).
694694
// Transcript cards wrap well before that, so a single rendered line no
695695
// longer holds the token. Join trimmed lines and recover the two digests.
696+
// The transcript may also retain an earlier partial command, so scan every
697+
// marker instead of rejecting after the first malformed candidate.
696698
let joined: String = text.lines().map(str::trim).collect();
697699
let marker = "/plugin trust demo ";
698-
let start = joined.find(marker)?;
699-
let token: String = joined[start + marker.len()..]
700-
.chars()
701-
.take_while(|ch| ch.is_ascii_hexdigit() || *ch == '.')
702-
.collect();
703-
let (content, capability) = token.split_once('.')?;
704-
(content.len() == 64
705-
&& capability.len() == 64
706-
&& content.chars().all(|ch| ch.is_ascii_hexdigit())
707-
&& capability.chars().all(|ch| ch.is_ascii_hexdigit()))
708-
.then(|| format!("{marker}{content}.{capability}"))
700+
joined.match_indices(marker).find_map(|(start, _)| {
701+
let token: String = joined[start + marker.len()..]
702+
.chars()
703+
.take_while(|ch| ch.is_ascii_hexdigit() || *ch == '.')
704+
.collect();
705+
let (content, capability) = token.split_once('.')?;
706+
(content.len() == 64
707+
&& capability.len() == 64
708+
&& content.chars().all(|ch| ch.is_ascii_hexdigit())
709+
&& capability.chars().all(|ch| ch.is_ascii_hexdigit()))
710+
.then(|| format!("{marker}{content}.{capability}"))
711+
})
709712
}
710713

711714
#[cfg(all(unix, feature = "long-running-tests"))]
@@ -970,11 +973,11 @@ async fn plugin_toml_binary_lifecycle_skill_and_stdio_mcp_acceptance() {
970973

971974
#[cfg(all(unix, feature = "long-running-tests"))]
972975
#[test]
973-
fn review_confirmation_survives_transcript_wrap() {
976+
fn review_confirmation_skips_partial_candidates_and_survives_transcript_wrap() {
974977
let content = "a".repeat(64);
975978
let capability = "b".repeat(64);
976979
let wrapped = format!(
977-
" /plugin trust demo {head}\n {mid}\n {tail}\n",
980+
"/plugin trust demo partial\n /plugin trust demo {head}\n {mid}\n {tail}\n",
978981
head = &format!("{content}.{capability}")[..40],
979982
mid = &format!("{content}.{capability}")[40..90],
980983
tail = &format!("{content}.{capability}")[90..],

0 commit comments

Comments
 (0)