use serde::Serialize;
use agentchrome::error::{AppError, ExitCode};
use crate::cli::OutputFormat;
pub const DEFAULT_THRESHOLD: usize = 16_384;
#[derive(Serialize)]
pub struct TempFileOutput {
pub output_file: String,
pub size_bytes: u64,
pub command: String,
pub summary: serde_json::Value,
}
pub fn write_temp_file(content: &str, extension: &str) -> Result<String, AppError> {
let id = uuid::Uuid::new_v4();
let filename = format!("agentchrome-{id}.{extension}");
let path = std::env::temp_dir().join(filename);
std::fs::write(&path, content).map_err(|e| AppError {
message: format!("failed to write temp file {}: {e}", path.display()),
code: ExitCode::GeneralError,
custom_json: None,
})?;
Ok(path.to_string_lossy().into_owned())
}
#[allow(clippy::needless_pass_by_value)]
fn serialization_error(e: serde_json::Error) -> AppError {
AppError {
message: format!("serialization error: {e}"),
code: ExitCode::GeneralError,
custom_json: None,
}
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)]
pub fn format_human_size(bytes: u64) -> String {
if bytes >= 1_048_576 {
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1024 {
format!("{} KB", bytes / 1024)
} else {
format!("{bytes} bytes")
}
}
pub fn emit_plain(text: &str, output: &OutputFormat) -> Result<(), AppError> {
let threshold = output.large_response_threshold.unwrap_or(DEFAULT_THRESHOLD);
if text.len() <= threshold {
print!("{text}");
return Ok(());
}
let path = write_temp_file(text, "txt")?;
println!("{path}");
Ok(())
}
pub fn emit<T, F>(
value: &T,
output: &OutputFormat,
command_name: &str,
summary_fn: F,
) -> Result<(), AppError>
where
T: Serialize,
F: FnOnce(&T) -> serde_json::Value,
{
let json_string = if output.pretty {
serde_json::to_string_pretty(value)
} else {
serde_json::to_string(value)
}
.map_err(serialization_error)?;
let threshold = output.large_response_threshold.unwrap_or(DEFAULT_THRESHOLD);
if json_string.len() <= threshold {
println!("{json_string}");
return Ok(());
}
let path = write_temp_file(&json_string, "json")?;
let summary = summary_fn(value);
#[allow(clippy::cast_possible_truncation)]
let size_bytes = json_string.len() as u64;
let temp_output = TempFileOutput {
output_file: path,
size_bytes,
command: command_name.to_string(),
summary,
};
let output_json = serde_json::to_string(&temp_output).map_err(serialization_error)?;
println!("{output_json}");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_human_size_bytes() {
assert_eq!(format_human_size(500), "500 bytes");
assert_eq!(format_human_size(0), "0 bytes");
assert_eq!(format_human_size(1023), "1023 bytes");
}
#[test]
fn format_human_size_kb() {
assert_eq!(format_human_size(1024), "1 KB");
assert_eq!(format_human_size(16_384), "16 KB");
assert_eq!(format_human_size(1_048_575), "1023 KB");
}
#[test]
fn format_human_size_mb() {
assert_eq!(format_human_size(1_048_576), "1.0 MB");
assert_eq!(format_human_size(5_242_880), "5.0 MB");
}
#[test]
fn temp_file_output_serialization() {
let output = TempFileOutput {
output_file: "/tmp/agentchrome-abc.json".to_string(),
size_bytes: 32_768,
command: "page snapshot".to_string(),
summary: serde_json::json!({"total_nodes": 5000}),
};
let json: serde_json::Value = serde_json::to_value(&output).unwrap();
assert_eq!(json["output_file"], "/tmp/agentchrome-abc.json");
assert_eq!(json["size_bytes"], 32_768);
assert_eq!(json["command"], "page snapshot");
assert_eq!(json["summary"]["total_nodes"], 5000);
}
#[test]
fn temp_file_output_has_exactly_four_keys() {
let output = TempFileOutput {
output_file: "/tmp/test.json".to_string(),
size_bytes: 100,
command: "test".to_string(),
summary: serde_json::json!({}),
};
let json: serde_json::Value = serde_json::to_value(&output).unwrap();
let keys = json.as_object().unwrap();
assert_eq!(keys.len(), 4);
assert!(keys.contains_key("output_file"));
assert!(keys.contains_key("size_bytes"));
assert!(keys.contains_key("command"));
assert!(keys.contains_key("summary"));
}
#[test]
fn write_temp_file_creates_readable_file() {
let content = "hello temp file";
let path = write_temp_file(content, "txt").unwrap();
let read_back = std::fs::read_to_string(&path).unwrap();
assert_eq!(read_back, content);
assert!(path.contains("agentchrome-"));
assert!(path.ends_with(".txt"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_temp_file_json_extension() {
let content = r#"{"key":"value"}"#;
let path = write_temp_file(content, "json").unwrap();
assert!(path.ends_with(".json"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_temp_file_uuid_uniqueness() {
let path1 = write_temp_file("a", "txt").unwrap();
let path2 = write_temp_file("b", "txt").unwrap();
assert_ne!(path1, path2);
let _ = std::fs::remove_file(&path1);
let _ = std::fs::remove_file(&path2);
}
#[test]
fn emit_below_threshold_prints_json() {
let value = serde_json::json!({"key": "value"});
let output = OutputFormat {
json: true,
pretty: false,
plain: false,
large_response_threshold: Some(1_000_000),
};
let result = emit(&value, &output, "test", |_| serde_json::json!({}));
assert!(result.is_ok());
}
#[test]
fn emit_above_threshold_creates_temp_file() {
let large_string: String = "x".repeat(1000);
let value = serde_json::json!({"data": large_string});
let output = OutputFormat {
json: true,
pretty: false,
plain: false,
large_response_threshold: Some(10), };
let result = emit(
&value,
&output,
"test cmd",
|_| serde_json::json!({"test": true}),
);
assert!(result.is_ok());
}
#[test]
fn emit_plain_below_threshold() {
let output = OutputFormat {
json: false,
pretty: false,
plain: true,
large_response_threshold: Some(1_000_000),
};
let result = emit_plain("short text", &output);
assert!(result.is_ok());
}
#[test]
fn emit_plain_above_threshold() {
let output = OutputFormat {
json: false,
pretty: false,
plain: true,
large_response_threshold: Some(5), };
let result = emit_plain("this text is longer than 5 bytes", &output);
assert!(result.is_ok());
}
}