#![cfg(feature = "serde")]
#![allow(non_snake_case, non_upper_case_globals)]
#![allow(clippy::default_constructed_unit_structs)]
use std::sync::Mutex;
use serde_json::json;
use tokitai::tool;
use tokitai::{clear_current_version, set_current_version, ToolCaller, ToolProvider};
static VERSION_LOCK: Mutex<()> = Mutex::new(());
#[allow(dead_code)]
fn lock_versions() -> std::sync::MutexGuard<'static, ()> {
VERSION_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[derive(Default)]
struct EvolvingTools;
#[tool]
impl EvolvingTools {
#[tool(since = "1.0", until = "2.0")]
pub fn add(&self, a: i64, b: i64) -> i64 {
a + b
}
#[tool(since = "2.0")]
pub fn mul(&self, a: i64, b: i64) -> i64 {
a * b
}
}
#[test]
fn test_since_until_bake_into_definition() {
let _lock = lock_versions();
clear_current_version();
let tools = EvolvingTools::tool_definitions();
let add = tools.iter().find(|t| t.name == "add").unwrap();
assert_eq!(add.since.as_deref(), Some("1.0"));
assert_eq!(add.until.as_deref(), Some("2.0"));
let mul = tools.iter().find(|t| t.name == "mul").unwrap();
assert_eq!(mul.since.as_deref(), Some("2.0"));
assert_eq!(mul.until, None);
}
#[test]
fn test_current_version_one_point_five_exposes_only_add() {
let _lock = lock_versions();
set_current_version("1.5");
let tools = EvolvingTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(
names.contains(&"add"),
"add must be served at v1.5: {:?}",
names
);
assert!(
!names.contains(&"mul"),
"mul (since=2.0) must NOT be served at v1.5: {:?}",
names
);
}
#[test]
fn test_current_version_two_exposes_only_mul() {
let _lock = lock_versions();
set_current_version("2.0");
let tools = EvolvingTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(
!names.contains(&"add"),
"add (until=2.0) must NOT be served at v2.0: {:?}",
names
);
assert!(
names.contains(&"mul"),
"mul must be served at v2.0: {:?}",
names
);
}
#[test]
fn test_current_version_two_point_five_exposes_only_mul() {
let _lock = lock_versions();
set_current_version("2.5");
let tools = EvolvingTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"mul"));
assert!(!names.contains(&"add"));
}
#[test]
fn test_no_current_version_serves_all_tools() {
let _lock = lock_versions();
clear_current_version();
let tools = EvolvingTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"add"));
assert!(names.contains(&"mul"));
}
#[test]
fn test_calls_to_active_tool_still_work() {
let _lock = lock_versions();
set_current_version("2.5");
let inst = EvolvingTools::default();
let result =
<EvolvingTools as ToolCaller>::call_tool(&inst, "mul", &json!({"a": 3, "b": 4})).unwrap();
assert_eq!(result, json!(12));
}
#[test]
fn test_filtered_tool_not_in_static_slice_but_still_callable() {
let _lock = lock_versions();
set_current_version("1.5");
let inst = EvolvingTools::default();
let tools = EvolvingTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(!names.contains(&"mul"), "mul must be hidden at v1.5");
let result =
<EvolvingTools as ToolCaller>::call_tool(&inst, "mul", &json!({"a": 3, "b": 4})).unwrap();
assert_eq!(result, json!(12));
}
#[derive(Default)]
struct AdditiveEvolution;
#[tool]
impl AdditiveEvolution {
#[tool(since = "1.0", until = "2.0")]
pub fn query_v1(&self, a: i64) -> i64 {
a
}
#[tool(since = "2.0")]
pub fn query_v2(&self, a: i64, b: Option<i64>) -> i64 {
a + b.unwrap_or(0)
}
}
#[test]
fn test_additive_change_is_non_breaking() {
let _lock = lock_versions();
set_current_version("1.0");
let tools = AdditiveEvolution::tool_definitions();
let v1 = tools.iter().find(|t| t.name == "query_v1").unwrap();
assert!(v1.since.is_some() && v1.until.is_some());
let schema: serde_json::Value = serde_json::from_str(&v1.input_schema).unwrap();
let props = schema["properties"].as_object().unwrap();
assert!(props.contains_key("a"));
assert!(!props.contains_key("b"));
}
#[test]
fn test_call_to_v1_method_at_v1_works() {
let _lock = lock_versions();
set_current_version("1.0");
let inst = AdditiveEvolution::default();
let result =
<AdditiveEvolution as ToolCaller>::call_tool(&inst, "query_v1", &json!({"a": 7})).unwrap();
assert_eq!(result, json!(7));
}
#[derive(Default)]
struct StrictSemVerTools;
#[tool(version_policy = "semver")]
impl StrictSemVerTools {
#[tool(since = "1.0.0", until = "2.0.0")]
pub fn v1_tool(&self) -> i64 {
1
}
#[tool(since = "2.0.0")]
pub fn v2_tool(&self) -> i64 {
2
}
}
#[test]
fn test_semver_ordered_interval_check() {
let _lock = lock_versions();
set_current_version("1.10.0");
let tools = StrictSemVerTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"v1_tool"));
assert!(!names.contains(&"v2_tool"));
}
#[test]
fn test_semver_equal_boundary_excludes_until_bound() {
let _lock = lock_versions();
set_current_version("2.0.0");
let tools = StrictSemVerTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(!names.contains(&"v1_tool"));
assert!(names.contains(&"v2_tool"));
}
#[derive(Default)]
struct LooseVersioningTools;
#[tool]
impl LooseVersioningTools {
#[tool(since = "2026.06")]
pub fn june_release(&self) -> i64 {
6
}
#[tool(since = "abc1234")]
pub fn dev_build(&self) -> i64 {
42
}
}
#[test]
fn test_loose_calver_strings_accepted() {
let _lock = lock_versions();
clear_current_version();
let tools = LooseVersioningTools::tool_definitions();
let june = tools.iter().find(|t| t.name == "june_release").unwrap();
assert_eq!(june.since.as_deref(), Some("2026.06"));
let dev = tools.iter().find(|t| t.name == "dev_build").unwrap();
assert_eq!(dev.since.as_deref(), Some("abc1234"));
}
#[test]
fn test_loose_strings_lexicographic_dispatch() {
let _lock = lock_versions();
set_current_version("2026.06");
let tools = LooseVersioningTools::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"june_release"));
assert!(!names.contains(&"dev_build"));
}
#[derive(Default)]
struct UnversionedAlongsideVersioned;
#[tool]
impl UnversionedAlongsideVersioned {
#[tool(since = "1.0", until = "2.0")]
pub fn versioned(&self) -> i64 {
1
}
pub fn always(&self) -> i64 {
99
}
}
#[test]
fn test_unversioned_method_always_served() {
let _lock = lock_versions();
set_current_version("1.5");
let tools = UnversionedAlongsideVersioned::tool_definitions();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"always"));
assert!(names.contains(&"versioned"));
}
#[cfg(feature = "serde")]
#[test]
fn test_version_interval_empty_compile_fail() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/version_interval_empty.rs");
}
#[cfg(feature = "serde")]
#[test]
fn test_version_interval_invalid_semver_compile_fail() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/version_interval_invalid_semver.rs");
}