PicoBot/src/tools/calculator.rs
oudecheng e0313ab8f3 fix: 增强 panic 安全与计算器健壮性
- agent_loop/processor: catch_unwind 隔离 panic,防止消息静默丢失
- calculator: 拒绝 NaN/Infinity 输入,修复阶乘溢出(上限 34),拒绝非有限表达式结果
- message: 修复 sanitize 两阶段删除索引未排序导致的越界 panic
- utils: 新增 panic_payload_message 提取可读 panic 消息
- .gitignore: 忽略 artifacts/ 测试产物目录
2026-08-13 08:38:36 +08:00

900 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.4e3834! 是最后一个不溢出的阶乘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"));
}
}