From dde865453e65569df1a24c5825671f2dd5c26d05 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Mon, 1 Jun 2026 02:36:50 -0700 Subject: [PATCH] test: cover Kimi schema and ANSI normalization edge cases --- crates/tui/src/tools/schema_sanitize.rs | 54 +++++++++++++++++++++++-- crates/tui/src/tui/footer_ui.rs | 9 ++++- crates/tui/src/tui/sidebar.rs | 10 ++++- crates/tui/src/tui/ui_text.rs | 10 +++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/tui/src/tools/schema_sanitize.rs b/crates/tui/src/tools/schema_sanitize.rs index 5409dafa..61798d22 100644 --- a/crates/tui/src/tools/schema_sanitize.rs +++ b/crates/tui/src/tools/schema_sanitize.rs @@ -618,6 +618,12 @@ mod tests { /// are left untouched. pub fn sanitize_for_kimi(schema: &mut serde_json::Value) { if let Some(obj) = schema.as_object_mut() { + // Recurse first so a type injected into this object's alternatives is + // not immediately removed again by processing that freshly-mutated item. + for (_, v) in obj.iter_mut() { + sanitize_for_kimi(v); + } + // If this object has `type` + `anyOf`/`oneOf`, push `type` into // each item and remove it from the parent. Otherwise leave it alone. let should_push = @@ -635,10 +641,6 @@ pub fn sanitize_for_kimi(schema: &mut serde_json::Value) { } } } - // Recurse into all sub-objects and arrays - for (_, v) in obj.iter_mut() { - sanitize_for_kimi(v); - } } else if let Some(arr) = schema.as_array_mut() { for v in arr.iter_mut() { sanitize_for_kimi(v); @@ -676,6 +678,50 @@ mod kimi_tests { assert_eq!(any_of[1]["type"], "null"); } + #[test] + fn kimi_sanitize_injects_missing_anyof_item_types() { + let mut schema = json!({ + "type": "object", + "anyOf": [ + {"properties": {"path": {"type": "string"}}}, + {"required": ["url"], "properties": {"url": {"type": "string"}}} + ] + }); + + sanitize_for_kimi(&mut schema); + + assert!( + !schema.as_object().unwrap().contains_key("type"), + "parent type should be removed" + ); + let any_of = schema["anyOf"].as_array().unwrap(); + assert_eq!(any_of[0]["type"], "object"); + assert_eq!(any_of[1]["type"], "object"); + } + + #[test] + fn kimi_sanitize_preserves_type_injected_into_nested_anyof_item() { + let mut schema = json!({ + "type": "object", + "anyOf": [ + { + "anyOf": [ + {"properties": {"path": {"type": "string"}}} + ] + } + ] + }); + + sanitize_for_kimi(&mut schema); + + let outer_item = &schema["anyOf"][0]; + assert_eq!(outer_item["type"], "object"); + assert!( + !schema.as_object().unwrap().contains_key("type"), + "outer parent type should be removed" + ); + } + #[test] fn kimi_sanitize_leaves_pure_object_untouched() { let original = json!({ diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 3995eba5..455a230a 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -181,7 +181,7 @@ pub(crate) fn footer_working_label_frame(now_ms: u64, fancy_animations: bool) -> #[cfg(test)] mod tests { - use super::footer_working_label_frame; + use super::{footer_working_label_frame, one_line_summary}; #[test] fn footer_working_label_frame_is_static_without_fancy_animations() { @@ -190,6 +190,13 @@ mod tests { assert_eq!(footer_working_label_frame(1_600, false), 0); assert_eq!(footer_working_label_frame(1_600, true), 4); } + + #[test] + fn one_line_summary_strips_ansi_before_collapsing_text() { + let summary = one_line_summary("read \x1b[38;2;6;174;242mfile.rs\x1b[0m", 80); + assert_eq!(summary, "read file.rs"); + assert!(!summary.contains("38;2")); + } } pub(crate) fn is_noisy_subagent_progress(status: &str) -> bool { diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index 1821c54c..fbf5e985 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -1955,7 +1955,8 @@ mod tests { AutoSidebarState, SidebarAgentRow, SidebarHoverSection, SidebarHoverState, SidebarSubagentSummary, SidebarToolRow, SidebarWorkChecklistItem, SidebarWorkStrategyStep, SidebarWorkSummary, ToolRowOrder, auto_sidebar_panels, editorial_tool_rows, - subagent_panel_lines, task_panel_lines, work_panel_empty_hint, work_panel_lines, + normalize_activity_text, subagent_panel_lines, task_panel_lines, work_panel_empty_hint, + work_panel_lines, }; use crate::config::Config; use crate::palette::PaletteMode; @@ -2033,6 +2034,13 @@ mod tests { ); } + #[test] + fn normalize_activity_text_strips_ansi_before_collapsing_text() { + let text = normalize_activity_text("running \x1b[48;2;10;17;32mtool\x1b[0m now"); + assert_eq!(text, "running tool now"); + assert!(!text.contains("48;2")); + } + #[test] fn editorial_rows_hide_older_failure_after_newer_success() { let rows = vec![ diff --git a/crates/tui/src/tui/ui_text.rs b/crates/tui/src/tui/ui_text.rs index c6ae8ed6..74893c7c 100644 --- a/crates/tui/src/tui/ui_text.rs +++ b/crates/tui/src/tui/ui_text.rs @@ -286,4 +286,14 @@ mod tests { let label = concise_shell_command_label("cd /tmp/repo && cargo test --workspace", 80); assert_eq!(label, "cargo test --workspace"); } + + #[test] + fn concise_shell_command_label_strips_ansi_before_collapsing_text() { + let label = concise_shell_command_label( + "cd /repo && \x1b[38;2;6;174;242mcargo test\x1b[0m --workspace", + 80, + ); + assert_eq!(label, "cargo test --workspace"); + assert!(!label.contains("38;2")); + } }