fix(tui): guard tick_batch zero drain and remove needless async tests

- tick_batch(0) now returns empty instead of silently draining one line
- has_live_tail avoids Vec allocation via field comparison
- Convert 15 sync-only controller tests from tokio::test to #[test]
This commit is contained in:
Felipe Coury
2026-04-06 13:35:50 -03:00
committed by Felipe Coury
parent 2fd2259286
commit 4c9b69b75d
3 changed files with 74 additions and 35 deletions
+2
View File
@@ -4831,6 +4831,7 @@ mod tests {
message: "tiny width coverage for wrapped user history".to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
remote_image_urls: Vec::new(),
};
let agent_message_cell = AgentMessageCell::new(vec!["tiny width agent line".into()], true);
let reasoning_cell = ReasoningSummaryCell::new(
@@ -4967,6 +4968,7 @@ mod tests {
message: "hello".to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
remote_image_urls: Vec::new(),
}) as Arc<dyn HistoryCell>;
let head = Arc::new(AgentMessageCell::new(vec![Line::from("line 1")], true))
as Arc<dyn HistoryCell>;
+1 -3
View File
@@ -1485,9 +1485,7 @@ where
tag_start += 1;
}
if bytes
.get(tag_start)
.is_some_and(u8::is_ascii_alphabetic)
if bytes.get(tag_start).is_some_and(u8::is_ascii_alphabetic)
&& bytes
.get(tag_start + 1..)
.is_some_and(|suffix| suffix.contains(&b'>'))
+71 -32
View File
@@ -177,7 +177,13 @@ impl StreamCore {
/// Batch drain: dequeue up to `max_lines`, update the emitted count.
fn tick_batch(&mut self, max_lines: usize) -> Vec<Line<'static>> {
let step = self.state.drain_n(max_lines.max(1));
if max_lines == 0 {
return Vec::new();
}
let step = self.state.drain_n(max_lines);
if step.is_empty() {
return step;
}
let emitted_start = self.emitted_stable_len;
self.emitted_stable_len += step.len();
self.advance_emitted_source_boundary(emitted_start, self.emitted_stable_len);
@@ -205,6 +211,10 @@ impl StreamCore {
self.rendered_lines[start..].to_vec()
}
fn has_tail(&self) -> bool {
self.enqueued_stable_len < self.rendered_lines.len()
}
/// Update rendering width and rebuild queued stable lines for the new layout.
///
/// Re-renders once at the new width and remaps already-emitted progress by
@@ -550,7 +560,7 @@ impl StreamController {
}
pub(crate) fn has_live_tail(&self) -> bool {
!self.current_tail_lines().is_empty()
self.core.has_tail()
}
pub(crate) fn set_width(&mut self, width: Option<usize>) {
@@ -1161,6 +1171,35 @@ mod tests {
);
}
#[test]
fn controller_tick_batch_zero_is_noop() {
let mut ctrl = stream_controller(Some(80));
assert!(ctrl.push("line one\n"));
assert_eq!(ctrl.queued_lines(), 1);
let (cell, idle) = ctrl.on_commit_tick_batch(0);
assert!(cell.is_none(), "batch size 0 should not emit lines");
assert!(!idle, "batch size 0 should not drain queued lines");
assert_eq!(
ctrl.queued_lines(),
1,
"queue depth should remain unchanged"
);
}
#[test]
fn controller_has_live_tail_reflects_tail_presence() {
let mut ctrl = stream_controller(Some(80));
assert!(!ctrl.has_live_tail());
ctrl.core.rendered_lines = vec![Line::from("tail line")];
ctrl.core.enqueued_stable_len = 0;
assert!(ctrl.has_live_tail());
ctrl.core.enqueued_stable_len = 1;
assert!(!ctrl.has_live_tail());
}
#[test]
fn controller_set_width_partial_drain_no_lost_lines() {
let mut ctrl = stream_controller(Some(40));
@@ -1257,8 +1296,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_loose_vs_tight_with_commit_ticks_matches_full() {
#[test]
fn controller_loose_vs_tight_with_commit_ticks_matches_full() {
let mut ctrl = stream_controller(/*width*/ None);
let mut lines = Vec::new();
@@ -1375,8 +1414,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_streamed_table_matches_full_render_widths() {
#[test]
fn controller_streamed_table_matches_full_render_widths() {
let deltas = vec![
"| Key | Description |\n",
"| --- | --- |\n",
@@ -1394,8 +1433,8 @@ mod tests {
assert_eq!(streamed, expected);
}
#[tokio::test]
async fn controller_holds_blockquoted_table_tail_until_stable() {
#[test]
fn controller_holds_blockquoted_table_tail_until_stable() {
let deltas = vec![
"> | A | B |\n",
"> | --- | --- |\n",
@@ -1477,8 +1516,8 @@ mod tests {
assert_eq!(streamed, expected);
}
#[tokio::test]
async fn controller_does_not_hold_back_pipe_prose_without_table_delimiter() {
#[test]
fn controller_does_not_hold_back_pipe_prose_without_table_delimiter() {
let mut ctrl = stream_controller(Some(80));
ctrl.push("status | owner | note\n");
@@ -1515,8 +1554,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_handles_table_immediately_after_heading() {
#[test]
fn controller_handles_table_immediately_after_heading() {
let deltas = vec![
"### 1) Basic table\n",
"| Name | Role | Status |\n",
@@ -1536,8 +1575,8 @@ mod tests {
assert_eq!(streamed, expected);
}
#[tokio::test]
async fn controller_renders_unicode_for_multi_table_response_shape() {
#[test]
fn controller_renders_unicode_for_multi_table_response_shape() {
let source = "Absolutely. Here are several different Markdown table patterns you can use for rendering tests.\n\n| Name | Role |
Location |\n|-------|-----------|----------|\n| Ava | Engineer | NYC |\n| Malik | Designer | Berlin |\n| Priya | PM | Remote
|\n\n| Item | Qty | Price | In Stock |\n|:------------|----:|------:|:--------:|\n| Keyboard | 2 | 49.99 | Yes |\n| Mouse | 10
@@ -1558,8 +1597,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_renders_unicode_for_no_outer_pipes_table_shape() {
#[test]
fn controller_renders_unicode_for_no_outer_pipes_table_shape() {
let source = "### 1) Basic\n\n| Name | Role | Active |\n|---|---|---|\n| Alice | Engineer | Yes |\n| Bob | Designer | No |\n\n### 2) No outer
pipes\n\nCol A | Col B | Col C\n--- | --- | ---\nx | y | z\n10 | 20 | 30\n\n### 3) Another table\n\n| Key | Value |\n|---|---|\n| a | b |\n";
@@ -1584,8 +1623,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_stabilizes_first_no_outer_pipes_table_in_response() {
#[test]
fn controller_stabilizes_first_no_outer_pipes_table_in_response() {
let deltas = vec![
"### No outer pipes first\n\n",
"Col A | Col B | Col C\n",
@@ -1615,8 +1654,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_stabilizes_two_column_no_outer_table_in_response() {
#[test]
fn controller_stabilizes_two_column_no_outer_table_in_response() {
let deltas = vec![
"A | B\n",
"--- | ---\n",
@@ -1642,8 +1681,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_converts_no_outer_table_between_preboxed_sections() {
#[test]
fn controller_converts_no_outer_table_between_preboxed_sections() {
let source = " ┌───────┬──────────┬────────┐\n │ Name │ Role │ Active │\n ├───────┼──────────┼────────┤\n │ Alice │ Engineer │ Yes │\n │ Bob │ Designer │ No │\n │ Cara │ PM │ Yes │\n └───────┴──────────┴────────┘\n\n ### 3) No outer pipes\n\n Col A | Col B | Col C\n --- | --- | ---\n x | y | z\n 10 | 20 | 30\n\n ┌─────────────────┬────────┬────────────────────────┐\n │ Example │ Output │ Notes │\n ├─────────────────┼────────┼────────────────────────┤\n │ a | b │ `a │ b` │\n │ npm run test │ ok │ Inline code formatting │\n │ SELECT * FROM t │ 3 rows │ SQL snippet │\n └─────────────────┴────────┴────────────────────────┘\n";
let deltas = source
@@ -1670,8 +1709,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_keeps_markdown_fenced_tables_mutable_until_finalize() {
#[test]
fn controller_keeps_markdown_fenced_tables_mutable_until_finalize() {
let source = "```md\n| A | B |\n|---|---|\n| 1 | 2 |\n```\n";
let deltas = vec![
"```md\n",
@@ -1697,8 +1736,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_keeps_markdown_fenced_no_outer_tables_mutable_until_finalize() {
#[test]
fn controller_keeps_markdown_fenced_no_outer_tables_mutable_until_finalize() {
let source =
"```md\nCol A | Col B | Col C\n--- | --- | ---\nx | y | z\n10 | 20 | 30\n```\n";
let deltas = vec![
@@ -1728,8 +1767,8 @@ mod tests {
);
}
#[tokio::test]
async fn controller_keeps_non_markdown_fenced_tables_as_code() {
#[test]
fn controller_keeps_non_markdown_fenced_tables_as_code() {
let source = "```sh\n| A | B |\n|---|---|\n| 1 | 2 |\n```\n";
let deltas = vec![
"```sh\n",
@@ -1755,8 +1794,8 @@ mod tests {
);
}
#[tokio::test]
async fn plan_controller_streamed_table_matches_finalize_render() {
#[test]
fn plan_controller_streamed_table_matches_finalize_render() {
let deltas = vec![
"## Build plan\n\n",
"| Step | Owner |\n",
@@ -1783,8 +1822,8 @@ mod tests {
);
}
#[tokio::test]
async fn plan_controller_streamed_markdown_fenced_table_matches_finalize_render() {
#[test]
fn plan_controller_streamed_markdown_fenced_table_matches_finalize_render() {
let deltas = vec![
"## Build plan\n\n",
"```md\n",