1use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Debug, Clone)]
15pub struct ToolResult {
16 pub success: bool,
17 pub output: String,
18 pub tool_name: String,
19}
20
21const NATIVE_EXECUTOR_TOOLS: &[&str] = &[
29 "Calculator",
30 "DateTimeTool",
31 "DocumentRenderer",
32 "DocumentReader",
34 "DocumentEditor",
35 "PDFExtractor",
39 "ImageTextExtractor",
40 "ImageAnalyzer",
41];
42
43pub fn dispatch(tool_name: &str, argument: &str) -> Option<ToolResult> {
48 match tool_name {
49 "Calculator" => Some(calculator_execute(argument)),
50 "DateTimeTool" => Some(datetime_execute(argument)),
51 "DocumentRenderer" => Some(document_render_execute(argument)),
55 "DocumentReader" => Some(document_read_execute(argument)),
58 "DocumentEditor" => Some(document_edit_execute(argument)),
61 "PDFExtractor" => Some(extraction_execute("pdf:text", argument)),
67 "ImageTextExtractor" => Some(extraction_execute("image:text", argument)),
68 "ImageAnalyzer" => Some(extraction_execute("image:description", argument)),
69 _ => None, }
71}
72
73pub fn dispatch_or_reject(tool_name: &str, argument: &str) -> Result<Option<ToolResult>, String> {
81 if let Some(r) = dispatch(tool_name, argument) {
82 return Ok(Some(r));
83 }
84 let declared = crate::stdlib::TOOLS.iter().any(|t| t.name == tool_name);
86 if declared && !NATIVE_EXECUTOR_TOOLS.contains(&tool_name) {
87 let has_provider = crate::stdlib::TOOLS
92 .iter()
93 .find(|t| t.name == tool_name)
94 .map(|t| !t.provider.is_empty() || t.requires_api_key)
95 .unwrap_or(false);
96 if !has_provider {
97 return Err(format!(
98 "tool '{tool_name}' is declared in the stdlib catalog but has no native \
99 implementation and no provider — refusing to silently hand the call to the \
100 model, which would fabricate its output (§Fase 99 §8). Implement the tool, give \
101 it a provider, or remove it from the catalog."
102 ));
103 }
104 }
105 Ok(None)
106}
107
108fn document_render_execute(argument: &str) -> ToolResult {
113 use base64::Engine;
114 let name = "DocumentRenderer";
115 let parsed: serde_json::Value = match serde_json::from_str(argument) {
116 Ok(v) => v,
117 Err(e) => return err(name, format!("invalid render request JSON: {e}")),
118 };
119 let doc_val = match parsed.get("document") {
120 Some(d) => d.clone(),
121 None => return err(name, "render request missing `document`".into()),
122 };
123 let spec: crate::ooxml::DocumentSpec = match serde_json::from_value(doc_val) {
124 Ok(s) => s,
125 Err(e) => return err(name, format!("malformed document IR: {e}")),
126 };
127 let values: std::collections::BTreeMap<String, String> = parsed
128 .get("values")
129 .and_then(|v| serde_json::from_value(v.clone()).ok())
130 .unwrap_or_default();
131 match crate::ooxml::render(&spec, &values) {
132 Ok(out) => {
133 let b64 = base64::engine::general_purpose::STANDARD.encode(&out.bytes);
134 let descriptor = serde_json::json!({
135 "sha256": out.sha256_hex,
136 "content_type": out.content_type,
137 "extension": out.extension,
138 "size_bytes": out.bytes.len(),
139 "bytes_base64": b64,
140 });
141 ToolResult {
142 success: true,
143 output: descriptor.to_string(),
144 tool_name: name.to_string(),
145 }
146 }
147 Err(e) => err(name, e.to_string()),
148 }
149}
150
151fn extraction_execute(transform: &str, argument: &str) -> ToolResult {
162 use base64::Engine;
163 let name = match transform {
164 "pdf:text" => "PDFExtractor",
165 "image:text" => "ImageTextExtractor",
166 _ => "ImageAnalyzer",
167 };
168 let parsed: serde_json::Value = match serde_json::from_str(argument) {
169 Ok(v) => v,
170 Err(e) => return err(name, format!("invalid extraction request JSON: {e}")),
171 };
172 let bytes = match parsed.get("bytes_base64").and_then(|v| v.as_str()) {
173 Some(b64) => match base64::engine::general_purpose::STANDARD.decode(b64) {
174 Ok(b) => b,
175 Err(e) => return err(name, format!("bytes_base64 is not valid base64: {e}")),
176 },
177 None => return err(name, "extraction request missing `bytes_base64`".into()),
178 };
179 let str_field = |k: &str| parsed.get(k).and_then(|v| v.as_str()).map(|s| s.to_string());
180 let hint = crate::extraction::ExtractionHint {
181 format: str_field("format"),
182 transform: Some(transform.to_string()),
183 target_field: str_field("target_field"),
184 language: str_field("language"),
185 };
186 match crate::extraction::run_active(&bytes, &hint, &crate::extraction::ExtractionBounds::default())
187 {
188 Ok(result) => {
189 let spans: Vec<serde_json::Value> = result
190 .spans
191 .iter()
192 .map(|s| {
193 serde_json::json!({
194 "text": s.text,
195 "confidence": s.confidence,
196 "page": s.page,
197 "bbox": { "x": s.bbox.x, "y": s.bbox.y, "w": s.bbox.w, "h": s.bbox.h },
198 })
199 })
200 .collect();
201 let out = serde_json::json!({
202 "engine": result.engine,
203 "engine_version": result.engine_version,
204 "transform": transform,
205 "provenance": "inferred", "taint": "untrusted", "epistemic_ceiling": "believe", "mean_confidence": result.mean_confidence(),
209 "page_count": result.page_count(),
210 "spans": spans,
211 });
212 ToolResult { success: true, output: out.to_string(), tool_name: name.to_string() }
213 }
214 Err(e) => err(name, format!("{}: {}", e.slug(), e)),
216 }
217}
218
219fn err(tool: &str, msg: String) -> ToolResult {
220 ToolResult {
221 success: false,
222 output: format!("DocumentRenderer: {msg}"),
223 tool_name: tool.to_string(),
224 }
225}
226
227fn document_read_execute(argument: &str) -> ToolResult {
232 use base64::Engine;
233 let name = "DocumentReader";
234 let parsed: serde_json::Value = match serde_json::from_str(argument) {
235 Ok(v) => v,
236 Err(e) => return err_named(name, format!("invalid request JSON: {e}")),
237 };
238 let bytes: Vec<u8> = if let Some(b64) = parsed.get("bytes_base64").and_then(|v| v.as_str()) {
240 match base64::engine::general_purpose::STANDARD.decode(b64) {
241 Ok(b) => b,
242 Err(e) => return err_named(name, format!("invalid base64: {e}")),
243 }
244 } else if let Some(path) = parsed.get("path").and_then(|v| v.as_str()) {
245 let roots: Vec<std::path::PathBuf> = parsed
246 .get("roots")
247 .and_then(|v| v.as_array())
248 .map(|a| a.iter().filter_map(|x| x.as_str().map(std::path::PathBuf::from)).collect())
249 .unwrap_or_default();
250 let sandbox = crate::fs_sandbox::PathSandbox::new(roots);
251 match sandbox.resolve(path) {
252 Ok(resolved) => match std::fs::read(&resolved) {
253 Ok(b) => b,
254 Err(e) => return err_named(name, format!("read failed: {e}")),
255 },
256 Err(e) => return err_named(name, e.to_string()),
257 }
258 } else {
259 return err_named(name, "request needs `bytes_base64` or `path`".into());
260 };
261
262 match crate::ooxml_read::read_ooxml(&bytes, &crate::ooxml_read::IngestBounds::default()) {
263 Ok(doc) => {
264 let text: Vec<serde_json::Value> = doc
265 .text
266 .iter()
267 .map(|r| serde_json::json!({ "part": r.part, "text": r.text }))
268 .collect();
269 let out = serde_json::json!({
270 "format": doc.format,
271 "taint": doc.taint.as_str(),
273 "provenance": doc.provenance.as_str(),
274 "epistemic_ceiling": doc.provenance.epistemic_ceiling(),
275 "text": text,
276 "full_text": doc.full_text(),
277 "part_hashes": doc.part_hashes,
278 });
279 ToolResult { success: true, output: out.to_string(), tool_name: name.to_string() }
280 }
281 Err(e) => err_named(name, e.to_string()),
282 }
283}
284
285fn document_edit_execute(argument: &str) -> ToolResult {
290 use base64::Engine;
291 let name = "DocumentEditor";
292 let parsed: serde_json::Value = match serde_json::from_str(argument) {
293 Ok(v) => v,
294 Err(e) => return err_named(name, format!("invalid request JSON: {e}")),
295 };
296 let b64 = match parsed.get("bytes_base64").and_then(|v| v.as_str()) {
297 Some(b) => b,
298 None => return err_named(name, "request needs `bytes_base64`".into()),
299 };
300 let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
301 Ok(b) => b,
302 Err(e) => return err_named(name, format!("invalid base64: {e}")),
303 };
304 let doc = match crate::ooxml_read::read_ooxml(&bytes, &crate::ooxml_read::IngestBounds::default()) {
305 Ok(d) => d,
306 Err(e) => return err_named(name, format!("read failed: {e}")),
307 };
308 let mut edits = Vec::new();
310 for e in parsed.get("edits").and_then(|v| v.as_array()).cloned().unwrap_or_default() {
311 let part = e.get("part").and_then(|v| v.as_str()).unwrap_or("").to_string();
312 match e.get("kind").and_then(|v| v.as_str()) {
313 Some("replace_text") => edits.push(crate::ooxml_edit::PartEdit::ReplaceText {
314 part,
315 find: e.get("find").and_then(|v| v.as_str()).unwrap_or("").to_string(),
316 replace: e.get("replace").and_then(|v| v.as_str()).unwrap_or("").to_string(),
317 }),
318 Some("replace") => {
319 let nb = e
320 .get("new_bytes_base64")
321 .and_then(|v| v.as_str())
322 .and_then(|s| base64::engine::general_purpose::STANDARD.decode(s).ok())
323 .unwrap_or_default();
324 edits.push(crate::ooxml_edit::PartEdit::Replace { part, new_bytes: nb });
325 }
326 other => return err_named(name, format!("unknown edit kind '{}'", other.unwrap_or("<none>"))),
327 }
328 }
329 match crate::ooxml_edit::edit_document(&doc, &edits) {
330 Ok(out) => {
331 let manifest: Vec<serde_json::Value> = out
332 .manifest
333 .iter()
334 .map(|m| serde_json::json!({
335 "part": m.part, "before": m.before_sha256, "after": m.after_sha256, "touched": m.touched,
336 }))
337 .collect();
338 let result = serde_json::json!({
339 "sha256": out.sha256_hex,
340 "taint": out.taint.as_str(),
342 "touched_parts": out.touched_parts(),
343 "manifest": manifest,
344 "bytes_base64": base64::engine::general_purpose::STANDARD.encode(&out.bytes),
345 });
346 ToolResult { success: true, output: result.to_string(), tool_name: name.to_string() }
347 }
348 Err(e) => err_named(name, e.to_string()),
349 }
350}
351
352fn err_named(tool: &str, msg: String) -> ToolResult {
353 ToolResult { success: false, output: format!("{tool}: {msg}"), tool_name: tool.to_string() }
354}
355
356pub fn calculator_execute(expression: &str) -> ToolResult {
364 let expr = expression.trim();
365 if expr.is_empty() {
366 return ToolResult {
367 success: false,
368 output: "Empty expression".to_string(),
369 tool_name: "Calculator".to_string(),
370 };
371 }
372
373 match eval_expr(expr) {
374 Ok(val) => {
375 let formatted = if val.fract() == 0.0 && val.abs() < 1e15 {
377 format!("{}", val as i64)
378 } else {
379 format!("{}", val)
380 };
381 ToolResult {
382 success: true,
383 output: formatted,
384 tool_name: "Calculator".to_string(),
385 }
386 }
387 Err(e) => ToolResult {
388 success: false,
389 output: format!("Calculator error: {e}"),
390 tool_name: "Calculator".to_string(),
391 },
392 }
393}
394
395struct CalcParser<'a> {
398 input: &'a [u8],
399 pos: usize,
400}
401
402impl<'a> CalcParser<'a> {
403 fn new(input: &'a str) -> Self {
404 Self {
405 input: input.as_bytes(),
406 pos: 0,
407 }
408 }
409
410 fn skip_ws(&mut self) {
411 while self.pos < self.input.len() && self.input[self.pos].is_ascii_whitespace() {
412 self.pos += 1;
413 }
414 }
415
416 fn peek(&mut self) -> Option<u8> {
417 self.skip_ws();
418 self.input.get(self.pos).copied()
419 }
420
421 fn consume(&mut self, expected: u8) -> bool {
422 self.skip_ws();
423 if self.pos < self.input.len() && self.input[self.pos] == expected {
424 self.pos += 1;
425 true
426 } else {
427 false
428 }
429 }
430
431 fn parse_expr(&mut self) -> Result<f64, String> {
433 let mut result = self.parse_term()?;
434 loop {
435 self.skip_ws();
436 if self.consume(b'+') {
437 result += self.parse_term()?;
438 } else if self.consume(b'-') {
439 result -= self.parse_term()?;
440 } else {
441 break;
442 }
443 }
444 Ok(result)
445 }
446
447 fn parse_term(&mut self) -> Result<f64, String> {
449 let mut result = self.parse_power()?;
450 loop {
451 self.skip_ws();
452 if self.consume(b'*') {
453 if self.consume(b'*') {
454 self.pos -= 2;
456 break;
457 }
458 result *= self.parse_power()?;
459 } else if self.consume(b'/') {
460 let divisor = self.parse_power()?;
461 if divisor == 0.0 {
462 return Err("Division by zero".to_string());
463 }
464 result /= divisor;
465 } else if self.consume(b'%') {
466 let modulus = self.parse_power()?;
467 if modulus == 0.0 {
468 return Err("Modulo by zero".to_string());
469 }
470 result %= modulus;
471 } else {
472 break;
473 }
474 }
475 Ok(result)
476 }
477
478 fn parse_power(&mut self) -> Result<f64, String> {
480 let base = self.parse_unary()?;
481 self.skip_ws();
482 if self.pos + 1 < self.input.len()
483 && self.input[self.pos] == b'*'
484 && self.input[self.pos + 1] == b'*'
485 {
486 self.pos += 2;
487 let exp = self.parse_power()?; Ok(base.powf(exp))
489 } else {
490 Ok(base)
491 }
492 }
493
494 fn parse_unary(&mut self) -> Result<f64, String> {
496 self.skip_ws();
497 if self.consume(b'-') {
498 Ok(-self.parse_unary()?)
499 } else if self.consume(b'+') {
500 self.parse_unary()
501 } else {
502 self.parse_atom()
503 }
504 }
505
506 fn parse_atom(&mut self) -> Result<f64, String> {
508 self.skip_ws();
509
510 if self.consume(b'(') {
512 let val = self.parse_expr()?;
513 if !self.consume(b')') {
514 return Err("Missing closing parenthesis".to_string());
515 }
516 return Ok(val);
517 }
518
519 if self.pos < self.input.len()
521 && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == b'.')
522 {
523 return self.parse_number();
524 }
525
526 if self.pos < self.input.len() && self.input[self.pos].is_ascii_alphabetic() {
528 let name = self.parse_ident();
529 return self.resolve_ident(&name);
530 }
531
532 Err(format!(
533 "Unexpected character at position {}",
534 self.pos
535 ))
536 }
537
538 fn parse_number(&mut self) -> Result<f64, String> {
539 let start = self.pos;
540 while self.pos < self.input.len()
541 && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == b'.')
542 {
543 self.pos += 1;
544 }
545 if self.pos < self.input.len()
547 && (self.input[self.pos] == b'e' || self.input[self.pos] == b'E')
548 {
549 self.pos += 1;
550 if self.pos < self.input.len()
551 && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-')
552 {
553 self.pos += 1;
554 }
555 while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() {
556 self.pos += 1;
557 }
558 }
559 let s = std::str::from_utf8(&self.input[start..self.pos])
560 .map_err(|_| "Invalid UTF-8 in number")?;
561 s.parse::<f64>()
562 .map_err(|_| format!("Invalid number: '{s}'"))
563 }
564
565 fn parse_ident(&mut self) -> String {
566 let start = self.pos;
567 while self.pos < self.input.len()
568 && (self.input[self.pos].is_ascii_alphanumeric() || self.input[self.pos] == b'_')
569 {
570 self.pos += 1;
571 }
572 String::from_utf8_lossy(&self.input[start..self.pos]).to_string()
573 }
574
575 fn resolve_ident(&mut self, name: &str) -> Result<f64, String> {
576 match name {
578 "pi" | "PI" => return Ok(std::f64::consts::PI),
579 "e" | "E" => return Ok(std::f64::consts::E),
580 "tau" | "TAU" => return Ok(std::f64::consts::TAU),
581 "inf" => return Ok(f64::INFINITY),
582 _ => {}
583 }
584
585 self.skip_ws();
587 if !self.consume(b'(') {
588 return Err(format!("Unknown identifier: '{name}'"));
589 }
590
591 let args = self.parse_args()?;
592
593 if !self.consume(b')') {
594 return Err(format!("Missing ')' after {name}(...)"));
595 }
596
597 match (name, args.len()) {
598 ("sqrt", 1) => Ok(args[0].sqrt()),
599 ("abs", 1) => Ok(args[0].abs()),
600 ("round", 1) => Ok(args[0].round()),
601 ("ceil", 1) => Ok(args[0].ceil()),
602 ("floor", 1) => Ok(args[0].floor()),
603 ("sin", 1) => Ok(args[0].sin()),
604 ("cos", 1) => Ok(args[0].cos()),
605 ("tan", 1) => Ok(args[0].tan()),
606 ("asin", 1) => Ok(args[0].asin()),
607 ("acos", 1) => Ok(args[0].acos()),
608 ("atan", 1) => Ok(args[0].atan()),
609 ("log", 1) | ("log10", 1) => Ok(args[0].log10()),
610 ("ln", 1) => Ok(args[0].ln()),
611 ("log2", 1) => Ok(args[0].log2()),
612 ("exp", 1) => Ok(args[0].exp()),
613 ("pow", 2) => Ok(args[0].powf(args[1])),
614 ("min", 2) => Ok(args[0].min(args[1])),
615 ("max", 2) => Ok(args[0].max(args[1])),
616 ("atan2", 2) => Ok(args[0].atan2(args[1])),
617 _ => Err(format!("Unknown function: '{name}' with {} args", args.len())),
618 }
619 }
620
621 fn parse_args(&mut self) -> Result<Vec<f64>, String> {
622 let mut args = Vec::new();
623 self.skip_ws();
624 if self.peek() == Some(b')') {
625 return Ok(args);
626 }
627 args.push(self.parse_expr()?);
628 while self.consume(b',') {
629 args.push(self.parse_expr()?);
630 }
631 Ok(args)
632 }
633}
634
635fn eval_expr(expr: &str) -> Result<f64, String> {
636 let mut parser = CalcParser::new(expr);
637 let result = parser.parse_expr()?;
638 parser.skip_ws();
639 if parser.pos < parser.input.len() {
640 return Err(format!(
641 "Unexpected trailing characters at position {}",
642 parser.pos
643 ));
644 }
645 if result.is_nan() {
646 return Err("Result is NaN".to_string());
647 }
648 Ok(result)
649}
650
651pub fn datetime_execute(query: &str) -> ToolResult {
658 let query = query.trim().to_lowercase();
659
660 let now = SystemTime::now()
661 .duration_since(UNIX_EPOCH)
662 .unwrap_or_default();
663
664 let secs = now.as_secs();
665 let (year, month, day, hour, min, sec, weekday) = unix_to_utc(secs);
666
667 let output = match query.as_str() {
668 "now" | "iso" | "datetime" => format!(
669 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
670 year, month, day, hour, min, sec
671 ),
672 "today" | "date" => format!("{:04}-{:02}-{:02}", year, month, day),
673 "time" => format!("{:02}:{:02}:{:02}Z", hour, min, sec),
674 "timestamp" | "unix" | "epoch" => format!("{}", secs),
675 "year" => format!("{}", year),
676 "month" => format!("{}", month),
677 "day" => format!("{}", day),
678 "hour" => format!("{}", hour),
679 "minute" => format!("{}", min),
680 "second" => format!("{}", sec),
681 "weekday" => weekday_name(weekday).to_string(),
682 _ => format!(
683 "Unknown query '{}'. Supported: now, today, timestamp, year, month, day, weekday, iso, time, hour, minute, second",
684 query
685 ),
686 };
687
688 ToolResult {
689 success: true,
690 output,
691 tool_name: "DateTimeTool".to_string(),
692 }
693}
694
695fn unix_to_utc(secs: u64) -> (i32, u32, u32, u32, u32, u32, u32) {
698 let days = (secs / 86400) as i64;
699 let time_of_day = secs % 86400;
700
701 let hour = (time_of_day / 3600) as u32;
702 let min = ((time_of_day % 3600) / 60) as u32;
703 let sec = (time_of_day % 60) as u32;
704
705 let weekday = ((days + 4) % 7) as u32;
707
708 let z = days + 719468;
710 let era = if z >= 0 { z } else { z - 146096 } / 146097;
711 let doe = (z - era * 146097) as u32;
712 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
713 let y = (yoe as i64 + era * 400) as i32;
714 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
715 let mp = (5 * doy + 2) / 153;
716 let d = doy - (153 * mp + 2) / 5 + 1;
717 let m = if mp < 10 { mp + 3 } else { mp - 9 };
718 let year = if m <= 2 { y + 1 } else { y };
719
720 (year, m, d, hour, min, sec, weekday)
721}
722
723fn weekday_name(weekday: u32) -> &'static str {
724 match weekday {
725 0 => "Sunday",
726 1 => "Monday",
727 2 => "Tuesday",
728 3 => "Wednesday",
729 4 => "Thursday",
730 5 => "Friday",
731 6 => "Saturday",
732 _ => "Unknown",
733 }
734}
735
736#[cfg(test)]
739mod tests {
740 use super::*;
741
742 #[test]
745 fn calc_basic_arithmetic() {
746 assert_eq!(eval_expr("2 + 3").unwrap(), 5.0);
747 assert_eq!(eval_expr("10 - 4").unwrap(), 6.0);
748 assert_eq!(eval_expr("3 * 7").unwrap(), 21.0);
749 assert_eq!(eval_expr("20 / 4").unwrap(), 5.0);
750 }
751
752 #[test]
753 fn calc_operator_precedence() {
754 assert_eq!(eval_expr("2 + 3 * 4").unwrap(), 14.0);
755 assert_eq!(eval_expr("(2 + 3) * 4").unwrap(), 20.0);
756 }
757
758 #[test]
759 fn calc_power() {
760 assert_eq!(eval_expr("2 ** 10").unwrap(), 1024.0);
761 assert_eq!(eval_expr("3 ** 2").unwrap(), 9.0);
762 }
763
764 #[test]
765 fn calc_modulo() {
766 assert_eq!(eval_expr("17 % 5").unwrap(), 2.0);
767 }
768
769 #[test]
770 fn calc_unary_minus() {
771 assert_eq!(eval_expr("-5").unwrap(), -5.0);
772 assert_eq!(eval_expr("-3 + 7").unwrap(), 4.0);
773 assert_eq!(eval_expr("-(2 + 3)").unwrap(), -5.0);
774 }
775
776 #[test]
777 fn calc_constants() {
778 assert!((eval_expr("pi").unwrap() - std::f64::consts::PI).abs() < 1e-10);
779 assert!((eval_expr("e").unwrap() - std::f64::consts::E).abs() < 1e-10);
780 }
781
782 #[test]
783 fn calc_functions() {
784 assert_eq!(eval_expr("sqrt(16)").unwrap(), 4.0);
785 assert_eq!(eval_expr("abs(-5)").unwrap(), 5.0);
786 assert_eq!(eval_expr("round(3.7)").unwrap(), 4.0);
787 assert_eq!(eval_expr("ceil(3.2)").unwrap(), 4.0);
788 assert_eq!(eval_expr("floor(3.8)").unwrap(), 3.0);
789 assert_eq!(eval_expr("pow(2, 8)").unwrap(), 256.0);
790 assert_eq!(eval_expr("min(3, 7)").unwrap(), 3.0);
791 assert_eq!(eval_expr("max(3, 7)").unwrap(), 7.0);
792 }
793
794 #[test]
795 fn calc_trig() {
796 assert!((eval_expr("sin(0)").unwrap()).abs() < 1e-10);
797 assert!((eval_expr("cos(0)").unwrap() - 1.0).abs() < 1e-10);
798 }
799
800 #[test]
801 fn calc_logarithm() {
802 assert!((eval_expr("log(100)").unwrap() - 2.0).abs() < 1e-10);
803 assert!((eval_expr("ln(e)").unwrap() - 1.0).abs() < 1e-10);
804 }
805
806 #[test]
807 fn calc_nested() {
808 assert_eq!(eval_expr("sqrt(pow(3, 2) + pow(4, 2))").unwrap(), 5.0);
809 }
810
811 #[test]
812 fn calc_scientific_notation() {
813 assert_eq!(eval_expr("1e3").unwrap(), 1000.0);
814 assert_eq!(eval_expr("2.5e2").unwrap(), 250.0);
815 }
816
817 #[test]
818 fn calc_division_by_zero() {
819 assert!(eval_expr("1 / 0").is_err());
820 }
821
822 #[test]
823 fn calc_empty_expression() {
824 let r = calculator_execute("");
825 assert!(!r.success);
826 }
827
828 #[test]
829 fn calc_invalid_expression() {
830 assert!(eval_expr("2 +").is_err());
831 }
832
833 #[test]
834 fn calc_integer_output() {
835 let r = calculator_execute("2 + 3");
836 assert!(r.success);
837 assert_eq!(r.output, "5");
838 }
839
840 #[test]
841 fn calc_float_output() {
842 let r = calculator_execute("1 / 3");
843 assert!(r.success);
844 assert!(r.output.starts_with("0.333"));
845 }
846
847 #[test]
850 fn datetime_now_iso_format() {
851 let r = datetime_execute("now");
852 assert!(r.success);
853 assert!(r.output.contains('T'));
854 assert!(r.output.ends_with('Z'));
855 }
856
857 #[test]
858 fn datetime_today() {
859 let r = datetime_execute("today");
860 assert!(r.success);
861 assert_eq!(r.output.len(), 10); assert!(r.output.contains('-'));
863 }
864
865 #[test]
866 fn datetime_timestamp() {
867 let r = datetime_execute("timestamp");
868 assert!(r.success);
869 let ts: u64 = r.output.parse().expect("should be a number");
870 assert!(ts > 1700000000); }
872
873 #[test]
874 fn datetime_year() {
875 let r = datetime_execute("year");
876 assert!(r.success);
877 let y: i32 = r.output.parse().expect("should be a number");
878 assert!(y >= 2024);
879 }
880
881 #[test]
882 fn datetime_weekday() {
883 let r = datetime_execute("weekday");
884 assert!(r.success);
885 let valid = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
886 assert!(valid.contains(&r.output.as_str()));
887 }
888
889 #[test]
890 fn datetime_unknown_query() {
891 let r = datetime_execute("foobar");
892 assert!(r.success);
893 assert!(r.output.contains("Unknown query"));
894 }
895
896 #[test]
899 fn dispatch_calculator() {
900 let r = dispatch("Calculator", "2 + 2");
901 assert!(r.is_some());
902 let r = r.unwrap();
903 assert!(r.success);
904 assert_eq!(r.output, "4");
905 }
906
907 #[test]
908 fn dispatch_datetime() {
909 let r = dispatch("DateTimeTool", "now");
910 assert!(r.is_some());
911 assert!(r.unwrap().success);
912 }
913
914 #[test]
915 fn dispatch_unknown_tool() {
916 assert!(dispatch("WebSearch", "query").is_none());
917 }
918}