- agent_loop/processor: catch_unwind 隔离 panic,防止消息静默丢失 - calculator: 拒绝 NaN/Infinity 输入,修复阶乘溢出(上限 34),拒绝非有限表达式结果 - message: 修复 sanitize 两阶段删除索引未排序导致的越界 panic - utils: 新增 panic_payload_message 提取可读 panic 消息 - .gitignore: 忽略 artifacts/ 测试产物目录
900 lines
30 KiB
Rust
900 lines
30 KiB
Rust
use super::traits::{Tool, ToolResult};
|
||
use crate::tools::check_null_args;
|
||
use crate::tools::extract_f64 as extract_f64_opt;
|
||
use async_trait::async_trait;
|
||
use serde_json::json;
|
||
|
||
pub struct CalculatorTool;
|
||
|
||
impl CalculatorTool {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
}
|
||
|
||
impl Default for CalculatorTool {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[async_trait]
|
||
impl Tool for CalculatorTool {
|
||
fn name(&self) -> &str {
|
||
"calculator"
|
||
}
|
||
|
||
fn description(&self) -> &str {
|
||
"Perform arithmetic and statistical calculations. Supports expression evaluation (evaluate) and functions: \
|
||
round, log, factorial, sum, average, median, mode, min, max, \
|
||
range, variance, stdev, percentile, count, percentage_change, clamp. \
|
||
Use this tool whenever you need to compute a numeric result instead of guessing."
|
||
}
|
||
|
||
fn read_only(&self) -> bool {
|
||
true
|
||
}
|
||
|
||
fn parameters_schema(&self) -> serde_json::Value {
|
||
json!({
|
||
"type": "object",
|
||
"properties": {
|
||
"function": {
|
||
"type": "string",
|
||
"description": "Calculation to perform. \
|
||
Expression: evaluate(expression) - supports +, -, *, /, %, ^, parentheses, and functions like sqrt, abs, exp, ln, sin, cos, tan, round, floor, ceil, max, min, etc. \
|
||
Rounding: round(x, decimals). \
|
||
Logarithmic: log(x, base?) - base defaults to 10. \
|
||
Special: factorial(x). \
|
||
Aggregation: sum(values), average(values), count(values), min(values), max(values), range(values). \
|
||
Statistics: median(values), mode(values), variance(values), stdev(values), percentile(values,p). \
|
||
Utility: percentage_change(a,b), clamp(x,min_val,max_val).",
|
||
"enum": [
|
||
"round", "log", "factorial",
|
||
"sum", "average", "median", "mode", "min", "max", "range",
|
||
"variance", "stdev", "percentile", "count",
|
||
"percentage_change", "clamp", "evaluate"
|
||
]
|
||
},
|
||
"values": {
|
||
"type": "array",
|
||
"items": { "type": "number" },
|
||
"description": "Array of numeric values. Required for: sum, average, median, mode, min, max, range, variance, stdev, percentile, count."
|
||
},
|
||
"a": {
|
||
"type": "number",
|
||
"description": "First operand. Required for: percentage_change."
|
||
},
|
||
"b": {
|
||
"type": "number",
|
||
"description": "Second operand. Required for: percentage_change."
|
||
},
|
||
"x": {
|
||
"type": "number",
|
||
"description": "Input number. Required for: log, factorial, round, clamp."
|
||
},
|
||
"base": {
|
||
"type": "number",
|
||
"description": "Logarithm base (default: 10). Optional for: log."
|
||
},
|
||
"decimals": {
|
||
"type": "integer",
|
||
"description": "Number of decimal places for rounding. Required for: round."
|
||
},
|
||
"p": {
|
||
"type": "integer",
|
||
"description": "Percentile rank (0-100). Required for: percentile."
|
||
},
|
||
"min_val": {
|
||
"type": "number",
|
||
"description": "Minimum bound. Required for: clamp."
|
||
},
|
||
"max_val": {
|
||
"type": "number",
|
||
"description": "Maximum bound. Required for: clamp."
|
||
},
|
||
"expression": {
|
||
"type": "string",
|
||
"description": "Mathematical expression to evaluate. Supports: +, -, *, /, %, ^ (power), parentheses. Functions: sqrt, abs, exp, ln, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh, floor, ceil, round, signum, max, min. Constants: pi, e. Variables: x, weight, etc. Example: '15*3+5^(2+1)', 'sin(pi/2)', 'max(1,2,3)'"
|
||
}
|
||
},
|
||
"required": ["function"]
|
||
})
|
||
}
|
||
|
||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||
if let Some(result) = check_null_args(&args, "calculator") {
|
||
return Ok(result);
|
||
}
|
||
|
||
let function = match args.get("function").and_then(|v| v.as_str()) {
|
||
Some(f) => f,
|
||
None => {
|
||
return Ok(ToolResult {
|
||
success: false,
|
||
output: String::new(),
|
||
error: Some("Missing required parameter: function".to_string()),
|
||
});
|
||
}
|
||
};
|
||
|
||
let result = match function {
|
||
"round" => calc_round(&args),
|
||
"log" => calc_log(&args),
|
||
"factorial" => calc_factorial(&args),
|
||
"sum" => calc_sum(&args),
|
||
"average" => calc_average(&args),
|
||
"median" => calc_median(&args),
|
||
"mode" => calc_mode(&args),
|
||
"min" => calc_min(&args),
|
||
"max" => calc_max(&args),
|
||
"range" => calc_range(&args),
|
||
"variance" => calc_variance(&args),
|
||
"stdev" => calc_stdev(&args),
|
||
"percentile" => calc_percentile(&args),
|
||
"count" => calc_count(&args),
|
||
"percentage_change" => calc_percentage_change(&args),
|
||
"clamp" => calc_clamp(&args),
|
||
"evaluate" => calc_evaluate(&args),
|
||
other => Err(format!("Unknown function: {other}")),
|
||
};
|
||
|
||
match result {
|
||
Ok(output) => Ok(ToolResult {
|
||
success: true,
|
||
output,
|
||
error: None,
|
||
}),
|
||
Err(err) => Ok(ToolResult {
|
||
success: false,
|
||
output: String::new(),
|
||
error: Some(err),
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn extract_f64(args: &serde_json::Value, key: &str, name: &str) -> Result<f64, String> {
|
||
match args.get(key) {
|
||
None => Err(format!("Missing required parameter: {name}")),
|
||
Some(v) => {
|
||
let n = if let Some(n) = v.as_f64() {
|
||
n
|
||
} else if let Some(s) = v.as_str() {
|
||
s.parse::<f64>()
|
||
.map_err(|_| format!("{name} is not a valid number: {s}"))?
|
||
} else {
|
||
return Err(format!("{name} must be a number"));
|
||
};
|
||
// f64::from_str 接受 "NaN"/"inf";非有限值会使 sort_by 的
|
||
// partial_cmp().unwrap() panic,且算术结果无意义,统一在边界拒绝。
|
||
if !n.is_finite() {
|
||
return Err(format!("{name} must be a finite number"));
|
||
}
|
||
Ok(n)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn extract_i64(args: &serde_json::Value, key: &str, name: &str) -> Result<i64, String> {
|
||
match args.get(key) {
|
||
None => Err(format!("Missing required parameter: {name}")),
|
||
Some(v) => {
|
||
if let Some(n) = v.as_i64() {
|
||
Ok(n)
|
||
} else if let Some(s) = v.as_str() {
|
||
s.parse::<i64>()
|
||
.map_err(|_| format!("{name} is not a valid integer: {s}"))
|
||
} else {
|
||
Err(format!("{name} must be an integer"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn extract_values(args: &serde_json::Value, min_len: usize) -> Result<Vec<f64>, String> {
|
||
let values = args
|
||
.get("values")
|
||
.and_then(|v| v.as_array())
|
||
.ok_or_else(|| "Missing required parameter: values (array of numbers)".to_string())?;
|
||
if values.len() < min_len {
|
||
return Err(format!(
|
||
"Expected at least {min_len} value(s), got {}",
|
||
values.len()
|
||
));
|
||
}
|
||
let mut nums = Vec::with_capacity(values.len());
|
||
for (i, v) in values.iter().enumerate() {
|
||
let n = if let Some(n) = v.as_f64() {
|
||
n
|
||
} else if let Some(s) = v.as_str() {
|
||
s.parse::<f64>()
|
||
.map_err(|_| format!("values[{i}] is not a valid number: {s}"))?
|
||
} else {
|
||
return Err(format!("values[{i}] is not a valid number"));
|
||
};
|
||
// f64::from_str 接受 "NaN"/"inf";非有限值会使 sort_by 的
|
||
// partial_cmp().unwrap() panic,且统计结果无意义,统一在边界拒绝。
|
||
if !n.is_finite() {
|
||
return Err(format!("values[{i}] is not a finite number"));
|
||
}
|
||
nums.push(n);
|
||
}
|
||
Ok(nums)
|
||
}
|
||
|
||
fn format_num(n: f64) -> String {
|
||
if n == n.floor() && n.abs() < 1e15 {
|
||
#[allow(clippy::cast_possible_truncation)]
|
||
let rounded = n.round() as i128;
|
||
format!("{rounded}")
|
||
} else {
|
||
format!("{n}")
|
||
}
|
||
}
|
||
|
||
fn calc_round(args: &serde_json::Value) -> Result<String, String> {
|
||
let x = extract_f64(args, "x", "x")?;
|
||
let decimals = extract_i64(args, "decimals", "decimals")?;
|
||
if decimals < 0 {
|
||
return Err("decimals must be non-negative".to_string());
|
||
}
|
||
let multiplier = 10_f64.powi(i32::try_from(decimals).unwrap_or(i32::MAX));
|
||
Ok(format_num((x * multiplier).round() / multiplier))
|
||
}
|
||
|
||
fn calc_log(args: &serde_json::Value) -> Result<String, String> {
|
||
let x = extract_f64(args, "x", "x")?;
|
||
if x <= 0.0 {
|
||
return Err("Logarithm requires a positive number".to_string());
|
||
}
|
||
let base = extract_f64_opt(args, "base").unwrap_or(10.0);
|
||
if base <= 0.0 || base == 1.0 {
|
||
return Err("Logarithm base must be positive and not equal to 1".to_string());
|
||
}
|
||
Ok(format_num(x.log(base)))
|
||
}
|
||
|
||
fn calc_factorial(args: &serde_json::Value) -> Result<String, String> {
|
||
let x = extract_f64(args, "x", "x")?;
|
||
if x < 0.0 || x != x.floor() {
|
||
return Err("Factorial requires a non-negative integer".to_string());
|
||
}
|
||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||
let n = x.round() as u128;
|
||
// u128::MAX ≈ 3.4e38,34! 是最后一个不溢出的阶乘(35! ≈ 1.03e40)。
|
||
// 修复前按 f64 范围放行到 170,实际在 n≥35 时 debug panic / release 静默回绕。
|
||
if n > 34 {
|
||
return Err("Factorial result exceeds supported integer range (max input: 34)".to_string());
|
||
}
|
||
let mut result: u128 = 1;
|
||
for i in 2..=n {
|
||
result = result
|
||
.checked_mul(i)
|
||
.ok_or_else(|| "Factorial result exceeds supported integer range".to_string())?;
|
||
}
|
||
Ok(result.to_string())
|
||
}
|
||
|
||
fn calc_sum(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
Ok(format_num(values.iter().sum()))
|
||
}
|
||
|
||
fn calc_average(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
if values.is_empty() {
|
||
return Err("Cannot compute average of an empty array".to_string());
|
||
}
|
||
Ok(format_num(values.iter().sum::<f64>() / values.len() as f64))
|
||
}
|
||
|
||
fn calc_median(args: &serde_json::Value) -> Result<String, String> {
|
||
let mut values = extract_values(args, 1)?;
|
||
if values.is_empty() {
|
||
return Err("Cannot compute median of an empty array".to_string());
|
||
}
|
||
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
let len = values.len();
|
||
if len % 2 == 0 {
|
||
Ok(format_num(f64::midpoint(
|
||
values[len / 2 - 1],
|
||
values[len / 2],
|
||
)))
|
||
} else {
|
||
Ok(format_num(values[len / 2]))
|
||
}
|
||
}
|
||
|
||
fn calc_mode(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
if values.is_empty() {
|
||
return Err("Cannot compute mode of an empty array".to_string());
|
||
}
|
||
let mut freq: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
|
||
for &v in &values {
|
||
let key = v.to_bits();
|
||
*freq.entry(key).or_insert(0) += 1;
|
||
}
|
||
let max_freq = *freq.values().max().unwrap();
|
||
let mut seen = std::collections::HashSet::new();
|
||
let mut modes = Vec::new();
|
||
for &v in &values {
|
||
let key = v.to_bits();
|
||
if freq[&key] == max_freq && seen.insert(key) {
|
||
modes.push(v);
|
||
}
|
||
}
|
||
if modes.len() == 1 {
|
||
Ok(format_num(modes[0]))
|
||
} else {
|
||
let formatted: Vec<String> = modes.iter().map(|v| format_num(*v)).collect();
|
||
Ok(format!("Modes: {}", formatted.join(", ")))
|
||
}
|
||
}
|
||
|
||
fn calc_min(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
let Some(min_val) = values.iter().copied().reduce(f64::min) else {
|
||
return Err("Cannot compute min of an empty array".to_string());
|
||
};
|
||
Ok(format_num(min_val))
|
||
}
|
||
|
||
fn calc_max(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
let Some(max_val) = values.iter().copied().reduce(f64::max) else {
|
||
return Err("Cannot compute max of an empty array".to_string());
|
||
};
|
||
Ok(format_num(max_val))
|
||
}
|
||
|
||
fn calc_range(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
if values.is_empty() {
|
||
return Err("Cannot compute range of an empty array".to_string());
|
||
}
|
||
let min_val = values.iter().copied().fold(f64::INFINITY, f64::min);
|
||
let max_val = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||
Ok(format_num(max_val - min_val))
|
||
}
|
||
|
||
fn calc_variance(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
if values.len() < 2 {
|
||
return Err("Variance requires at least 2 values".to_string());
|
||
}
|
||
let mean = values.iter().sum::<f64>() / values.len() as f64;
|
||
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
|
||
Ok(format_num(variance))
|
||
}
|
||
|
||
fn calc_stdev(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
if values.len() < 2 {
|
||
return Err("Standard deviation requires at least 2 values".to_string());
|
||
}
|
||
let mean = values.iter().sum::<f64>() / values.len() as f64;
|
||
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
|
||
Ok(format_num(variance.sqrt()))
|
||
}
|
||
|
||
fn calc_percentile(args: &serde_json::Value) -> Result<String, String> {
|
||
let mut values = extract_values(args, 1)?;
|
||
if values.is_empty() {
|
||
return Err("Cannot compute percentile of an empty array".to_string());
|
||
}
|
||
let p = extract_i64(args, "p", "p (percentile rank 0-100)")?;
|
||
if !(0..=100).contains(&p) {
|
||
return Err("Percentile rank must be between 0 and 100".to_string());
|
||
}
|
||
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
|
||
let idx_f = p as f64 / 100.0 * (values.len() - 1) as f64;
|
||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||
let index = idx_f.round().clamp(0.0, (values.len() - 1) as f64) as usize;
|
||
Ok(format_num(values[index]))
|
||
}
|
||
|
||
fn calc_count(args: &serde_json::Value) -> Result<String, String> {
|
||
let values = extract_values(args, 1)?;
|
||
Ok(values.len().to_string())
|
||
}
|
||
|
||
fn calc_percentage_change(args: &serde_json::Value) -> Result<String, String> {
|
||
let old = extract_f64(args, "a", "a (old value)")?;
|
||
let new = extract_f64(args, "b", "b (new value)")?;
|
||
if old == 0.0 {
|
||
return Err("Cannot compute percentage change from zero".to_string());
|
||
}
|
||
Ok(format_num((new - old) / old.abs() * 100.0))
|
||
}
|
||
|
||
fn calc_clamp(args: &serde_json::Value) -> Result<String, String> {
|
||
let x = extract_f64(args, "x", "x")?;
|
||
let min_val = extract_f64(args, "min_val", "min_val")?;
|
||
let max_val = extract_f64(args, "max_val", "max_val")?;
|
||
if min_val > max_val {
|
||
return Err("min_val must be less than or equal to max_val".to_string());
|
||
}
|
||
Ok(format_num(x.clamp(min_val, max_val)))
|
||
}
|
||
|
||
fn calc_evaluate(args: &serde_json::Value) -> Result<String, String> {
|
||
let expression = args
|
||
.get("expression")
|
||
.and_then(|v| v.as_str())
|
||
.ok_or_else(|| "Missing required parameter: expression".to_string())?;
|
||
|
||
meval::eval_str(expression)
|
||
.map_err(|e| format!("Expression evaluation error: {e}"))
|
||
.and_then(|n| {
|
||
// 表达式可产生非有限结果(如 "1/0" → inf、"0/0" → NaN),
|
||
// 与 extract_values/extract_f64 的边界策略保持一致:拒绝输出。
|
||
if !n.is_finite() {
|
||
return Err(format!("Expression result is not a finite number: {expression}"));
|
||
}
|
||
Ok(format_num(n))
|
||
})
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[tokio::test]
|
||
async fn test_round() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "round", "x": 2.715, "decimals": 2}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2.72");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_log_base10() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "log", "x": 100.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_log_custom_base() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "log", "x": 8.0, "base": 2.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "3");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_factorial() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "factorial", "x": 5.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "120");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_average() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "average", "values": [10.0, 20.0, 30.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "20");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_median_odd() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "median", "values": [3.0, 1.0, 2.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_median_even() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "median", "values": [4.0, 1.0, 3.0, 2.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2.5");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_mode() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "mode", "values": [1.0, 2.0, 2.0, 3.0, 3.0, 3.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "3");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_min() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "min", "values": [5.0, 2.0, 8.0, 1.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "1");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_max() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "max", "values": [5.0, 2.0, 8.0, 1.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "8");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_range() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "range", "values": [1.0, 5.0, 10.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "9");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_variance() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(
|
||
json!({"function": "variance", "values": [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]}),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "4");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_stdev() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(
|
||
json!({"function": "stdev", "values": [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]}),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_percentile_50() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(
|
||
json!({"function": "percentile", "values": [1.0, 2.0, 3.0, 4.0, 5.0], "p": 50}),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "3");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_count() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "count", "values": [1.0, 2.0, 3.0, 4.0, 5.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "5");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_percentage_change() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "percentage_change", "a": 50.0, "b": 75.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "50");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_clamp_within_range() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "clamp", "x": 5.0, "min_val": 1.0, "max_val": 10.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "5");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_clamp_below_min() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "clamp", "x": -5.0, "min_val": 0.0, "max_val": 10.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "0");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_clamp_above_max() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "clamp", "x": 15.0, "min_val": 0.0, "max_val": 10.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "10");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_unknown_function() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool.execute(json!({"function": "unknown"})).await.unwrap();
|
||
assert!(!result.success);
|
||
assert!(result.error.as_ref().unwrap().contains("Unknown function"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_sum() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "sum", "values": [1.0, 2.0, 3.0, 4.0, 5.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "15");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_simple() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "15*3+5^(2+1)"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
// 15*3 + 5^(2+1) = 45 + 5^3 = 45 + 125 = 170
|
||
assert_eq!(result.output, "170");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_complex() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "(10-2)*(3+1)"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "32");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_invalid() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "invalid"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_missing_expression() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool.execute(json!({"function": "evaluate"})).await.unwrap();
|
||
assert!(!result.success);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_with_functions() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "sqrt(144)"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "12");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_with_constants() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "pi * 2"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "6.283185307179586");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_modulo() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "17 % 5"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_round_with_string_x() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "round", "x": "2.715", "decimals": "2"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "2.72");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_sum_with_string_values() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "sum", "values": ["1.5", "2.5", "3"]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(result.success);
|
||
assert_eq!(result.output, "7");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_invalid_string_number_returns_error() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "round", "x": "not_a_number", "decimals": 2}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(
|
||
result
|
||
.error
|
||
.as_ref()
|
||
.unwrap()
|
||
.contains("x is not a valid number")
|
||
);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_null_args_returns_error() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool.execute(serde_json::Value::Null).await.unwrap();
|
||
assert!(!result.success);
|
||
assert!(
|
||
result
|
||
.error
|
||
.as_ref()
|
||
.unwrap()
|
||
.contains("Missing required parameters")
|
||
);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_median_rejects_nan_string_value() {
|
||
// f64::from_str accepts "NaN"; partial_cmp on NaN is None and would
|
||
// panic inside sort_by — must surface as a tool error instead.
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "median", "values": ["NaN", 1.0, 2.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(
|
||
result.error.as_ref().unwrap().contains("finite"),
|
||
"expected finiteness error, got: {:?}",
|
||
result.error
|
||
);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_percentile_rejects_infinity_string_value() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "percentile", "values": ["inf", 1.0], "p": 50}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(result.error.as_ref().unwrap().contains("finite"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_sum_rejects_nan_instead_of_returning_nan() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "sum", "values": ["NaN", 1.0]}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(result.error.as_ref().unwrap().contains("finite"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_clamp_rejects_non_finite_scalar() {
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "clamp", "x": "NaN", "min_val": 0.0, "max_val": 1.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(result.error.as_ref().unwrap().contains("finite"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_factorial_large_input_returns_error_not_overflow() {
|
||
// 35! ≈ 1.03e40 超出 u128::MAX ≈ 3.4e38:修复前 debug 下乘法溢出 panic、
|
||
// release 下静默回绕。必须返回工具错误。
|
||
let tool = CalculatorTool::new();
|
||
let result = tool
|
||
.execute(json!({"function": "factorial", "x": 35.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!result.success);
|
||
assert!(
|
||
result.error.as_ref().unwrap().contains("range"),
|
||
"expected range error, got: {:?}",
|
||
result.error
|
||
);
|
||
// 34! 仍可精确计算
|
||
let ok = tool
|
||
.execute(json!({"function": "factorial", "x": 34.0}))
|
||
.await
|
||
.unwrap();
|
||
assert!(ok.success);
|
||
assert_eq!(
|
||
ok.output,
|
||
"295232799039604140847618609643520000000"
|
||
);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_evaluate_rejects_non_finite_result() {
|
||
let tool = CalculatorTool::new();
|
||
let division_by_zero = tool
|
||
.execute(json!({"function": "evaluate", "expression": "1/0"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!division_by_zero.success);
|
||
assert!(division_by_zero.error.as_ref().unwrap().contains("finite"));
|
||
|
||
let nan_result = tool
|
||
.execute(json!({"function": "evaluate", "expression": "0/0"}))
|
||
.await
|
||
.unwrap();
|
||
assert!(!nan_result.success);
|
||
assert!(nan_result.error.as_ref().unwrap().contains("finite"));
|
||
}
|
||
}
|