bzzz-core 0.1.0

Bzzz core library - Declarative orchestration engine for AI Agents
Documentation
//! Runtime implementations

pub mod a2a;
pub mod compose;
pub mod control;
pub mod docker;
pub mod factory;
pub mod health;
pub mod heartbeat;
pub mod http;
pub mod local;
pub mod logging;
pub mod metrics;
pub mod pool;
pub mod retry;
pub mod shutdown;
pub mod tracing;
pub mod watcher;

pub use a2a::*;
pub use compose::*;
pub use control::*;
pub use docker::*;
pub use factory::*;
pub use health::*;
pub use heartbeat::*;
pub use http::*;
pub use local::*;
pub use logging::*;
pub use metrics::*;
pub use pool::*;
pub use retry::*;
pub use shutdown::*;
pub use tracing::*;
pub use watcher::*;

/// Maximum stderr size to capture (8 KB), preventing memory bloat.
pub const MAX_STDERR_BYTES: usize = 8 * 1024;

/// Parse subprocess stdout + stderr into a JSON Value.
///
/// Handles 6 combinations:
/// - stdout=JSON,  stderr=empty     → parsed JSON (unchanged)
/// - stdout=JSON,  stderr=non-empty → parsed JSON + `"stderr"` field merged in
/// - stdout=text,  stderr=empty     → `{"stdout": "<text>"}`
/// - stdout=text,  stderr=non-empty → `{"stdout": "<text>", "stderr": "<stderr>"}`
/// - stdout=empty, stderr=empty     → `None`
/// - stdout=empty, stderr=non-empty → `{"stderr": "<stderr>"}`
pub fn parse_worker_output(stdout: &str, stderr: &str) -> Option<serde_json::Value> {
    let trimmed_out = stdout.trim();
    let trimmed_err = stderr.trim();

    match (trimmed_out.is_empty(), trimmed_err.is_empty()) {
        // Both empty → None
        (true, true) => None,
        // stdout empty, stderr present → {"stderr": "..."}
        (true, false) => Some(serde_json::json!({"stderr": trimmed_err})),
        // stdout present, stderr empty → parse stdout as before
        (false, true) => match serde_json::from_str(trimmed_out) {
            Ok(value) => Some(value),
            Err(_) => Some(serde_json::json!({"stdout": trimmed_out})),
        },
        // Both present → merge stderr into output
        (false, false) => match serde_json::from_str::<serde_json::Value>(trimmed_out) {
            Ok(mut value) => {
                if let Some(obj) = value.as_object_mut() {
                    obj.insert("stderr".into(), serde_json::Value::String(trimmed_err.into()));
                }
                Some(value)
            }
            Err(_) => Some(serde_json::json!({"stdout": trimmed_out, "stderr": trimmed_err})),
        },
    }
}

/// Truncate stderr to at most [`MAX_STDERR_BYTES`] bytes (UTF-8 safe).
pub fn truncate_stderr(s: &str) -> &str {
    if s.len() <= MAX_STDERR_BYTES {
        s
    } else {
        let mut boundary = MAX_STDERR_BYTES;
        while !s.is_char_boundary(boundary) {
            boundary -= 1;
        }
        &s[..boundary]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_worker_output_json() {
        let result = parse_worker_output(r#"{"key":"value"}"#, "");
        assert_eq!(result, Some(serde_json::json!({"key": "value"})));
    }

    #[test]
    fn test_parse_worker_output_plain_text() {
        let result = parse_worker_output("hello world", "");
        assert_eq!(result, Some(serde_json::json!({"stdout": "hello world"})));
    }

    #[test]
    fn test_parse_worker_output_empty() {
        assert_eq!(parse_worker_output("", ""), None);
        assert_eq!(parse_worker_output("   \n", ""), None);
    }

    #[test]
    fn test_parse_worker_output_json_with_whitespace() {
        let result = parse_worker_output("  {\"key\": 42}  \n", "");
        assert_eq!(result, Some(serde_json::json!({"key": 42})));
    }

    #[test]
    fn test_parse_worker_output_stderr_only() {
        let result = parse_worker_output("", "error: something went wrong");
        assert_eq!(
            result,
            Some(serde_json::json!({"stderr": "error: something went wrong"}))
        );
    }

    #[test]
    fn test_parse_worker_output_json_with_stderr() {
        let result = parse_worker_output(r#"{"result":1}"#, "warning: deprecated");
        assert_eq!(
            result,
            Some(serde_json::json!({"result": 1, "stderr": "warning: deprecated"}))
        );
    }

    #[test]
    fn test_parse_worker_output_text_with_stderr() {
        let result = parse_worker_output("hello", "some warning");
        assert_eq!(
            result,
            Some(serde_json::json!({"stdout": "hello", "stderr": "some warning"}))
        );
    }

    #[test]
    fn test_parse_worker_output_both_empty() {
        assert_eq!(parse_worker_output("", ""), None);
        assert_eq!(parse_worker_output("  ", "  "), None);
    }

    #[test]
    fn test_truncate_stderr_short() {
        let s = "short error";
        assert_eq!(truncate_stderr(s), s);
    }

    #[test]
    fn test_truncate_stderr_at_limit() {
        let s = "a".repeat(MAX_STDERR_BYTES);
        assert_eq!(truncate_stderr(&s).len(), MAX_STDERR_BYTES);
    }

    #[test]
    fn test_truncate_stderr_over_limit() {
        let s = "a".repeat(MAX_STDERR_BYTES + 100);
        assert_eq!(truncate_stderr(&s).len(), MAX_STDERR_BYTES);
    }
}