From 786df99dc7c33c60b7f09355898542de9ea983ee Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Mon, 16 Mar 2026 14:57:35 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20address=20review=20feedback=20=E2=80=94?= =?UTF-8?q?=20UTF-8=20safe=20truncation,=20saturating=5Fsub,=20lock=20pois?= =?UTF-8?q?on=20logging,=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- src/tools/builtin/ptc_script.rs | 24 +++++++++++++++++++++++- src/tools/registry.rs | 2 ++ src/tools/wasm/wrapper.rs | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/tools/builtin/ptc_script.rs b/src/tools/builtin/ptc_script.rs index 205ef603..2e75a6cc 100644 --- a/src/tools/builtin/ptc_script.rs +++ b/src/tools/builtin/ptc_script.rs @@ -118,9 +118,13 @@ impl PtcScriptTool { if output.len() <= MAX_OUTPUT_SIZE { output.to_string() } else { + let mut i = MAX_OUTPUT_SIZE; + while i > 0 && !output.is_char_boundary(i) { + i -= 1; + } format!( "{}\n\n[Output truncated at {} bytes]", - &output[..MAX_OUTPUT_SIZE], + &output[..i], MAX_OUTPUT_SIZE ) } @@ -335,6 +339,24 @@ mod tests { assert!(truncated.contains("[Output truncated")); } + #[test] + fn test_truncate_output_multibyte_boundary() { + // Build a string of multi-byte chars (emoji = 4 bytes each) that crosses MAX_OUTPUT_SIZE + let emoji = "\u{1F600}"; // 4 bytes + let count = MAX_OUTPUT_SIZE / emoji.len() + 10; + let long: String = emoji.repeat(count); + assert!(long.len() > MAX_OUTPUT_SIZE); + + let truncated = PtcScriptTool::truncate_output(&long); + // Must not panic and must contain valid UTF-8 + assert!(truncated.contains("[Output truncated")); + // The kept portion must end on a char boundary (valid UTF-8 guaranteed by compilation) + let kept = truncated.split("\n\n[Output truncated").next().unwrap(); + assert!(kept.len() <= MAX_OUTPUT_SIZE); + // Every char should be complete (no partial emoji) + assert!(kept.chars().all(|c| c == '\u{1F600}')); + } + #[test] fn test_tool_metadata() { let tool = PtcScriptTool::new(); diff --git a/src/tools/registry.rs b/src/tools/registry.rs index d5e92245..ca015325 100644 --- a/src/tools/registry.rs +++ b/src/tools/registry.rs @@ -157,6 +157,8 @@ impl ToolRegistry { pub fn set_tool_executor(&self, executor: Arc) { if let Ok(mut guard) = self.tool_executor_slot.write() { *guard = Some(executor); + } else { + tracing::error!("tool_executor_slot RwLock is poisoned; PTC will be unavailable"); } } diff --git a/src/tools/wasm/wrapper.rs b/src/tools/wasm/wrapper.rs index a1756d64..b98c6392 100644 --- a/src/tools/wasm/wrapper.rs +++ b/src/tools/wasm/wrapper.rs @@ -46,7 +46,7 @@ struct NestingGuard<'a> { impl Drop for NestingGuard<'_> { fn drop(&mut self) { - *self.depth -= 1; + *self.depth = self.depth.saturating_sub(1); } }