Skip to main content

nika_engine/runtime/builtin/
assert.rs

1//! nika:assert - Validate condition, fail if false.
2//!
3//! # Parameters
4//!
5//! ```json
6//! {
7//!   "condition": true,       // Boolean condition to assert
8//!   "message": "Expected X"  // Error message if assertion fails (optional)
9//! }
10//! ```
11//!
12//! # Returns (on success)
13//!
14//! ```json
15//! {
16//!   "passed": true,
17//!   "condition": true
18//! }
19//! ```
20//!
21//! # Error (on failure)
22//!
23//! Returns NIKA-213 AssertionFailed error with the provided message.
24
25use super::BuiltinTool;
26use crate::error::NikaError;
27use serde::{Deserialize, Serialize};
28use std::future::Future;
29use std::pin::Pin;
30
31/// Parameters for nika:assert tool.
32#[derive(Debug, Clone, Deserialize)]
33struct AssertParams {
34    /// The condition to assert (must be true to pass).
35    condition: bool,
36    /// Error message if assertion fails (optional).
37    #[serde(default)]
38    message: Option<String>,
39}
40
41/// Response from nika:assert tool (on success).
42#[derive(Debug, Clone, Serialize)]
43struct AssertResponse {
44    /// Whether the assertion passed.
45    passed: bool,
46    /// The condition value that was asserted.
47    condition: bool,
48}
49
50/// nika:assert builtin tool.
51///
52/// Validates a condition and fails the workflow if it's false.
53/// Useful for workflow invariants and preconditions.
54pub struct AssertTool;
55
56impl BuiltinTool for AssertTool {
57    fn name(&self) -> &'static str {
58        "assert"
59    }
60
61    fn description(&self) -> &'static str {
62        "Validate condition, fail workflow if false"
63    }
64
65    fn parameters_schema(&self) -> serde_json::Value {
66        // OpenAI-compatible schema with additionalProperties: false
67        serde_json::json!({
68            "type": "object",
69            "properties": {
70                "condition": {
71                    "type": "boolean",
72                    "description": "Boolean condition to assert (must be true)"
73                },
74                "message": {
75                    "type": "string",
76                    "description": "Error message if assertion fails"
77                }
78            },
79            "required": ["condition", "message"],
80            "additionalProperties": false
81        })
82    }
83
84    fn call<'a>(
85        &'a self,
86        args: String,
87    ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
88        Box::pin(async move {
89            // Parse parameters
90            let params: AssertParams =
91                serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
92                    tool: "nika:assert".into(),
93                    reason: format!("Invalid JSON parameters: {}", e),
94                })?;
95
96            // Check the condition
97            if !params.condition {
98                let message = params
99                    .message
100                    .unwrap_or_else(|| "Assertion failed".to_string());
101                return Err(NikaError::AssertionFailed {
102                    message,
103                    condition: "false".to_string(),
104                });
105            }
106
107            // Return success response
108            let response = AssertResponse {
109                passed: true,
110                condition: params.condition,
111            };
112
113            serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
114                tool: "nika:assert".into(),
115                reason: format!("Failed to serialize response: {}", e),
116            })
117        })
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_assert_tool_name() {
127        let tool = AssertTool;
128        assert_eq!(tool.name(), "assert");
129    }
130
131    #[test]
132    fn test_assert_tool_description() {
133        let tool = AssertTool;
134        assert!(tool.description().contains("condition"));
135    }
136
137    #[test]
138    fn test_assert_tool_schema() {
139        let tool = AssertTool;
140        let schema = tool.parameters_schema();
141        assert_eq!(schema["type"], "object");
142        assert!(schema["properties"]["condition"].is_object());
143        assert!(schema["properties"]["message"].is_object());
144        assert!(schema["required"]
145            .as_array()
146            .unwrap()
147            .contains(&serde_json::json!("condition")));
148    }
149
150    #[tokio::test]
151    async fn test_assert_true_passes() {
152        let tool = AssertTool;
153        let result = tool.call(r#"{"condition": true}"#.to_string()).await;
154
155        assert!(result.is_ok());
156        let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
157        assert_eq!(response["passed"], true);
158        assert_eq!(response["condition"], true);
159    }
160
161    #[tokio::test]
162    async fn test_assert_false_fails() {
163        let tool = AssertTool;
164        let result = tool.call(r#"{"condition": false}"#.to_string()).await;
165
166        assert!(result.is_err());
167        let err = result.unwrap_err();
168        assert!(err.to_string().contains("Assertion failed"));
169    }
170
171    #[tokio::test]
172    async fn test_assert_false_with_message() {
173        let tool = AssertTool;
174        let result = tool
175            .call(r#"{"condition": false, "message": "Expected X to equal Y"}"#.to_string())
176            .await;
177
178        assert!(result.is_err());
179        let err = result.unwrap_err();
180        assert!(err.to_string().contains("Expected X to equal Y"));
181    }
182
183    #[tokio::test]
184    async fn test_assert_true_with_message_still_passes() {
185        let tool = AssertTool;
186        let result = tool
187            .call(r#"{"condition": true, "message": "This should not appear"}"#.to_string())
188            .await;
189
190        assert!(result.is_ok());
191    }
192
193    #[tokio::test]
194    async fn test_assert_invalid_json() {
195        let tool = AssertTool;
196        let result = tool.call("not json".to_string()).await;
197
198        assert!(result.is_err());
199        let err = result.unwrap_err();
200        assert!(err.to_string().contains("Invalid JSON parameters"));
201    }
202
203    #[tokio::test]
204    async fn test_assert_missing_condition() {
205        let tool = AssertTool;
206        let result = tool.call(r#"{"message": "test"}"#.to_string()).await;
207
208        assert!(result.is_err());
209        let err = result.unwrap_err();
210        assert!(err.to_string().contains("Invalid JSON parameters"));
211    }
212
213    #[tokio::test]
214    async fn test_assert_wrong_condition_type() {
215        let tool = AssertTool;
216        let result = tool.call(r#"{"condition": "true"}"#.to_string()).await;
217
218        assert!(result.is_err());
219        let err = result.unwrap_err();
220        assert!(err.to_string().contains("Invalid JSON parameters"));
221    }
222
223    #[tokio::test]
224    async fn test_assert_null_condition() {
225        let tool = AssertTool;
226        let result = tool.call(r#"{"condition": null}"#.to_string()).await;
227
228        assert!(result.is_err());
229        let err = result.unwrap_err();
230        assert!(err.to_string().contains("Invalid JSON parameters"));
231    }
232
233    #[tokio::test]
234    async fn test_assert_error_code() {
235        let tool = AssertTool;
236        let result = tool.call(r#"{"condition": false}"#.to_string()).await;
237
238        assert!(result.is_err());
239        let err = result.unwrap_err();
240        // Check it's the AssertionFailed variant (NIKA-213)
241        assert!(err.to_string().contains("NIKA-213"));
242    }
243}