mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 08:39:24 +00:00
* refactor: extract AppEvent to crates/ironclaw_common SseEvent was defined in src/channels/web/types.rs but imported by 12+ modules across agent, orchestrator, worker, tools, and extensions — it had become the application-wide event protocol, not a web transport concern. Create crates/ironclaw_common as a shared workspace crate and move the enum there as AppEvent. Also move the truncate_preview utility which was similarly leaked from the web gateway into agent modules. - New crate: crates/ironclaw_common (AppEvent, truncate_preview) - Rename SseEvent → AppEvent, from_sse_event → from_app_event - web/types.rs re-exports AppEvent for internal gateway use - web/util.rs re-exports truncate_preview - Wire format unchanged (serde renames are on variants, not the enum) Aligned with the event bus direction on refactor/architectural-hardening where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: add AppEvent::event_type() helper, deduplicate match blocks Address Gemini review: extract the variant→string match into a single method on AppEvent, replacing the duplicated 22-arm matches in sse.rs and types.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: rename leftover sse vars/tests to match AppEvent rename Address Copilot review: rename sse_event vars to app_event in orchestrator/api.rs and ws.rs, rename test functions from test_ws_server_from_sse_* to test_ws_server_from_app_event_*, and update stale SSE comments. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: add Deserialize to AppEvent, round-trip test, fix stale comments Address zmanian review: - Add Deserialize derive to AppEvent so downstream consumers can deserialize incoming events - Add event_type_matches_serde_type_field test that round-trips every variant through serde and asserts event_type() matches the serialized "type" field — catches drift between serde renames and the manual match - Add round_trip_deserialize test for basic Serialize/Deserialize parity - Update remaining "SSE" references in comments across server.rs, manager.rs, ws_gateway_integration.rs, and worker/job.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
101 lines
2.9 KiB
Rust
101 lines
2.9 KiB
Rust
//! Shared utility functions.
|
|
|
|
/// Truncate a string to at most `max_bytes` bytes at a char boundary, appending "...".
|
|
///
|
|
/// If the input is wrapped in `<tool_output ...>...</tool_output>` and truncation
|
|
/// removes the closing tag, the tag is re-appended so downstream XML parsers
|
|
/// never see an unclosed element.
|
|
pub fn truncate_preview(s: &str, max_bytes: usize) -> String {
|
|
if s.len() <= max_bytes {
|
|
return s.to_string();
|
|
}
|
|
// Walk backwards from max_bytes to find a valid char boundary
|
|
let mut end = max_bytes;
|
|
while end > 0 && !s.is_char_boundary(end) {
|
|
end -= 1;
|
|
}
|
|
let mut result = format!("{}...", &s[..end]);
|
|
|
|
// Re-close <tool_output> if truncation cut through the closing tag.
|
|
if s.starts_with("<tool_output") && !result.ends_with("</tool_output>") {
|
|
result.push_str("\n</tool_output>");
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_truncate_preview_short_string() {
|
|
assert_eq!(truncate_preview("hello", 10), "hello");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_exact_boundary() {
|
|
assert_eq!(truncate_preview("hello", 5), "hello");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_truncates_ascii() {
|
|
assert_eq!(truncate_preview("hello world", 5), "hello...");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_empty_string() {
|
|
assert_eq!(truncate_preview("", 10), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_multibyte_char_boundary() {
|
|
let s = "a\u{20AC}b";
|
|
let result = truncate_preview(s, 3);
|
|
assert_eq!(result, "a...");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_emoji() {
|
|
let s = "hi\u{1F980}";
|
|
let result = truncate_preview(s, 4);
|
|
assert_eq!(result, "hi...");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_cjk() {
|
|
let s = "\u{4F60}\u{597D}\u{4E16}\u{754C}";
|
|
let result = truncate_preview(s, 7);
|
|
assert_eq!(result, "\u{4F60}\u{597D}...");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_zero_max_bytes() {
|
|
assert_eq!(truncate_preview("hello", 0), "...");
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_closes_tool_output_tag() {
|
|
let s = "<tool_output name=\"search\">\nSome very long content here\n</tool_output>";
|
|
let result = truncate_preview(s, 60);
|
|
assert!(result.ends_with("</tool_output>"));
|
|
assert!(result.contains("..."));
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_no_extra_close_when_intact() {
|
|
let s = "<tool_output name=\"echo\">\nshort\n</tool_output>";
|
|
let result = truncate_preview(s, 500);
|
|
assert_eq!(result, s);
|
|
assert_eq!(result.matches("</tool_output>").count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_preview_non_xml_unaffected() {
|
|
let s = "Just a plain long string that gets truncated";
|
|
let result = truncate_preview(s, 10);
|
|
assert_eq!(result, "Just a pla...");
|
|
assert!(!result.contains("</tool_output>"));
|
|
}
|
|
}
|