use crate::javascript::HttpMethod;
use crate::types::ExecutionError;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::{HashMap, HashSet};
use crate::eval::{
evaluate as shared_evaluate, evaluate_with_binding as shared_evaluate_with_binding,
evaluate_with_two_bindings as shared_evaluate_with_two_bindings, is_truthy as shared_is_truthy,
json_to_string_with_mode as shared_json_to_string_with_mode, JsonStringMode,
};
use swc_common::{FileName, SourceMap};
use swc_ecma_ast::*;
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
pub(crate) enum StepOutcome {
None,
Return(serde_json::Value),
Continue,
Break,
}
#[derive(Debug, Clone)]
pub struct ExecutionConfig {
pub max_api_calls: usize,
pub timeout_seconds: u64,
pub max_loop_iterations: usize,
pub blocked_fields: HashSet<String>,
pub output_blocked_fields: HashSet<String>,
}
impl Default for ExecutionConfig {
fn default() -> Self {
Self {
max_api_calls: 50,
timeout_seconds: 30,
max_loop_iterations: 100,
blocked_fields: HashSet::new(),
output_blocked_fields: HashSet::new(),
}
}
}
impl ExecutionConfig {
pub fn with_blocked_fields(
mut self,
fields: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.blocked_fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn with_output_blocked_fields(
mut self,
fields: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.output_blocked_fields = fields.into_iter().map(Into::into).collect();
self
}
}
pub fn filter_blocked_fields(value: JsonValue, blocked_fields: &HashSet<String>) -> JsonValue {
if blocked_fields.is_empty() {
return value;
}
match value {
JsonValue::Object(mut map) => {
map.retain(|key, _| !blocked_fields.contains(key));
let filtered: serde_json::Map<String, JsonValue> = map
.into_iter()
.map(|(k, v)| (k, filter_blocked_fields(v, blocked_fields)))
.collect();
JsonValue::Object(filtered)
},
JsonValue::Array(arr) => {
let filtered: Vec<JsonValue> = arr
.into_iter()
.map(|v| filter_blocked_fields(v, blocked_fields))
.collect();
JsonValue::Array(filtered)
},
other => other,
}
}
pub fn find_blocked_fields_in_output(
value: &JsonValue,
blocked_fields: &HashSet<String>,
) -> Vec<String> {
if blocked_fields.is_empty() {
return Vec::new();
}
let mut violations = Vec::new();
find_blocked_fields_recursive(value, blocked_fields, "", &mut violations);
violations
}
fn find_blocked_fields_recursive(
value: &JsonValue,
blocked_fields: &HashSet<String>,
path: &str,
violations: &mut Vec<String>,
) {
match value {
JsonValue::Object(map) => {
for (key, v) in map {
if blocked_fields.contains(key) {
if path.is_empty() {
violations.push(key.clone());
} else {
violations.push(format!("{}.{}", path, key));
}
}
let new_path = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
find_blocked_fields_recursive(v, blocked_fields, &new_path, violations);
}
},
JsonValue::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let new_path = if path.is_empty() {
format!("[{}]", i)
} else {
format!("{}[{}]", path, i)
};
find_blocked_fields_recursive(v, blocked_fields, &new_path, violations);
}
},
_ => {},
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ExecutionPlan {
pub steps: Vec<PlanStep>,
pub metadata: PlanMetadata,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanMetadata {
pub api_call_count: usize,
pub has_mutations: bool,
pub endpoints: Vec<String>,
pub methods_used: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub enum PlanStep {
ApiCall {
result_var: String,
method: String,
path: PathTemplate,
body: Option<ValueExpr>,
},
Assign { var: String, expr: ValueExpr },
Conditional {
condition: ValueExpr,
then_steps: Vec<PlanStep>,
else_steps: Vec<PlanStep>,
},
BoundedLoop {
item_var: String,
collection: ValueExpr,
max_iterations: usize,
body: Vec<PlanStep>,
},
Return { value: ValueExpr },
TryCatch {
try_steps: Vec<PlanStep>,
catch_var: Option<String>,
catch_steps: Vec<PlanStep>,
finally_steps: Vec<PlanStep>,
},
ParallelApiCalls {
result_var: String,
calls: Vec<(String, String, PathTemplate, Option<ValueExpr>)>, },
Continue,
Break,
#[cfg(feature = "mcp-code-mode")]
McpCall {
result_var: String,
server_id: String,
tool_name: String,
args: Option<ValueExpr>,
},
SdkCall {
result_var: String,
operation: String, args: Option<ValueExpr>, },
}
#[derive(Debug, Clone, Serialize)]
pub struct PathTemplate {
pub parts: Vec<PathPart>,
}
impl PathTemplate {
pub fn static_path(path: String) -> Self {
Self {
parts: vec![PathPart::Literal(path)],
}
}
pub fn is_dynamic(&self) -> bool {
self.parts
.iter()
.any(|p| matches!(p, PathPart::Variable(_) | PathPart::Expression(_)))
}
}
#[derive(Debug, Clone, Serialize)]
pub enum PathPart {
Literal(String),
Variable(String),
Expression(ValueExpr),
}
#[derive(Debug, Clone, Serialize)]
pub enum ValueExpr {
Literal(JsonValue),
Variable(String),
PropertyAccess {
object: Box<ValueExpr>,
property: String,
},
ArrayIndex {
array: Box<ValueExpr>,
index: Box<ValueExpr>,
},
ObjectLiteral { fields: Vec<ObjectField> },
ArrayLiteral { items: Vec<ValueExpr> },
ArrayMethod {
array: Box<ValueExpr>,
method: ArrayMethodCall,
},
NumberMethod {
number: Box<ValueExpr>,
method: NumberMethodCall,
},
BinaryOp {
left: Box<ValueExpr>,
op: BinaryOperator,
right: Box<ValueExpr>,
},
UnaryOp {
op: UnaryOperator,
operand: Box<ValueExpr>,
},
Ternary {
condition: Box<ValueExpr>,
consequent: Box<ValueExpr>,
alternate: Box<ValueExpr>,
},
OptionalChain {
object: Box<ValueExpr>,
property: String,
},
NullishCoalesce {
left: Box<ValueExpr>,
right: Box<ValueExpr>,
},
Await { expr: Box<ValueExpr> },
PromiseAll { items: Vec<ValueExpr> },
ApiCall {
method: String,
path: PathTemplate,
body: Option<Box<ValueExpr>>,
},
Block {
bindings: Vec<(String, ValueExpr)>,
result: Box<ValueExpr>,
},
#[cfg(feature = "mcp-code-mode")]
McpCall {
server_id: String,
tool_name: String,
args: Option<Box<ValueExpr>>,
},
SdkCall {
operation: String,
args: Option<Box<ValueExpr>>,
},
BuiltinCall {
func: BuiltinFunction,
args: Vec<ValueExpr>,
},
}
#[derive(Debug, Clone, Serialize)]
pub enum ObjectField {
KeyValue { key: String, value: ValueExpr },
Spread { expr: ValueExpr },
}
#[derive(Debug, Clone, Serialize)]
pub enum ArrayMethodCall {
Map {
item_var: String,
body: Box<ValueExpr>,
},
Filter {
item_var: String,
predicate: Box<ValueExpr>,
},
Find {
item_var: String,
predicate: Box<ValueExpr>,
},
Slice { start: usize, end: Option<usize> },
Length,
Some {
item_var: String,
predicate: Box<ValueExpr>,
},
Every {
item_var: String,
predicate: Box<ValueExpr>,
},
Reduce {
acc_var: String,
item_var: String,
body: Box<ValueExpr>,
initial: Box<ValueExpr>,
},
Push { item: Box<ValueExpr> },
Concat { other: Box<ValueExpr> },
Includes { item: Box<ValueExpr> },
IndexOf { item: Box<ValueExpr> },
Join { separator: Option<String> },
Reverse,
Sort {
comparator: Option<(String, String, Box<ValueExpr>)>,
},
Flat,
FlatMap {
item_var: String,
body: Box<ValueExpr>,
},
First,
Last,
ToLowerCase,
ToUpperCase,
StartsWith { search: Box<ValueExpr> },
EndsWith { search: Box<ValueExpr> },
Trim,
Replace {
search: Box<ValueExpr>,
replacement: Box<ValueExpr>,
},
Split { separator: Box<ValueExpr> },
Substring {
start: Box<ValueExpr>,
end: Option<Box<ValueExpr>>,
},
ToString,
}
#[derive(Debug, Clone, Serialize)]
pub enum NumberMethodCall {
ToFixed { digits: usize },
ToString,
}
#[derive(Debug, Clone, Serialize)]
pub enum BuiltinFunction {
ParseFloat,
ParseInt,
NumberCast,
MathAbs,
MathMax,
MathMin,
MathRound,
MathFloor,
MathCeil,
ObjectKeys,
ObjectValues,
ObjectEntries,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum BinaryOperator {
Add,
Sub,
Mul,
Div,
Mod,
BitwiseOr,
Eq,
NotEq,
StrictEq,
StrictNotEq,
Lt,
Lte,
Gt,
Gte,
And,
Or,
Concat,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum UnaryOperator {
Not,
Neg,
Plus,
TypeOf,
}
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error("Unsupported statement type: {0}")]
UnsupportedStatement(String),
#[error("Unsupported expression type: {0}")]
UnsupportedExpression(String),
#[error("Invalid API call: {0}")]
InvalidApiCall(String),
#[error("Unbounded loop detected")]
UnboundedLoop,
#[error("Invalid path template: {0}")]
InvalidPath(String),
#[error("Too many API calls in plan: {count} (max: {max})")]
TooManyApiCalls { count: usize, max: usize },
#[error("Unsupported array method: {0}")]
UnsupportedArrayMethod(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Missing variable name")]
MissingVariableName,
}
enum ExtractedCall {
Http {
method: String,
path: PathTemplate,
body: Option<ValueExpr>,
},
Sdk {
operation: String,
args: Option<ValueExpr>,
},
}
pub struct PlanCompiler {
max_api_calls: usize,
api_call_count: usize,
endpoints: Vec<String>,
methods_used: Vec<String>,
has_mutations: bool,
sdk_operations: HashSet<String>,
destructure_counter: usize,
}
impl PlanCompiler {
pub fn new() -> Self {
Self::with_config(&ExecutionConfig::default())
}
pub fn with_config(config: &ExecutionConfig) -> Self {
Self {
max_api_calls: config.max_api_calls,
api_call_count: 0,
endpoints: Vec::new(),
methods_used: Vec::new(),
has_mutations: false,
sdk_operations: HashSet::new(),
destructure_counter: 0,
}
}
pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
self.sdk_operations = operations;
self
}
pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
let cm = SourceMap::default();
let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
let lexer = Lexer::new(
Syntax::Es(Default::default()),
EsVersion::Es2022,
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().map_err(|e| {
CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
})?;
self.compile(&module)
}
pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
let mut steps = Vec::new();
for item in &module.body {
match item {
ModuleItem::Stmt(stmt) => {
self.compile_statement(stmt, &mut steps)?;
},
_ => {
return Err(CompileError::UnsupportedStatement(
"import/export not allowed".into(),
));
},
}
}
self.methods_used.sort();
self.methods_used.dedup();
Ok(ExecutionPlan {
steps,
metadata: PlanMetadata {
api_call_count: self.api_call_count,
has_mutations: self.has_mutations,
endpoints: self.endpoints.clone(),
methods_used: self.methods_used.clone(),
},
})
}
fn compile_statement(
&mut self,
stmt: &Stmt,
steps: &mut Vec<PlanStep>,
) -> Result<(), CompileError> {
match stmt {
Stmt::Decl(Decl::Var(var_decl)) => {
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
match &decl.name {
Pat::Ident(ident) => {
let var_name = ident.id.sym.to_string();
self.compile_var_init(&var_name, init, steps)?;
},
Pat::Object(obj_pat) => {
self.compile_object_destructuring(obj_pat, init, steps)?;
},
Pat::Array(arr_pat) => {
self.compile_array_destructuring(arr_pat, init, steps)?;
},
_ => {
return Err(CompileError::UnsupportedExpression(
"complex destructuring pattern".into(),
));
},
}
}
}
},
Stmt::Expr(expr_stmt) => {
if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
if assign.op == swc_ecma_ast::AssignOp::Assign {
if let Some(ident) = assign.left.as_ident() {
let var_name = ident.sym.to_string();
self.compile_var_init(&var_name, &assign.right, steps)?;
return Ok(());
}
}
}
let expr = self.compile_expr(&expr_stmt.expr)?;
match expr {
ValueExpr::ApiCall { method, path, body } => {
self.record_api_call(&method, &path);
steps.push(PlanStep::ApiCall {
result_var: "_".into(), method,
path,
body: body.map(|b| *b),
});
},
ValueExpr::SdkCall { operation, args } => {
steps.push(PlanStep::SdkCall {
result_var: "_".into(), operation,
args: args.map(|a| *a),
});
},
ValueExpr::ArrayMethod {
ref array,
ref method,
} if matches!(
method,
ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
) =>
{
if let ValueExpr::Variable(var_name) = array.as_ref() {
steps.push(PlanStep::Assign {
var: var_name.clone(),
expr,
});
}
},
_ => {},
}
},
Stmt::If(if_stmt) => {
let condition = self.compile_expr(&if_stmt.test)?;
let mut then_steps = Vec::new();
self.compile_statement(&if_stmt.cons, &mut then_steps)?;
let mut else_steps = Vec::new();
if let Some(alt) = &if_stmt.alt {
self.compile_statement(alt, &mut else_steps)?;
}
steps.push(PlanStep::Conditional {
condition,
then_steps,
else_steps,
});
},
Stmt::ForOf(for_of) => {
let (item_var, destructure_bindings) = match &for_of.left {
ForHead::VarDecl(decl) => {
if let Some(first) = decl.decls.first() {
self.extract_loop_var(&first.name)?
} else {
return Err(CompileError::MissingVariableName);
}
},
ForHead::Pat(pat) => self.extract_loop_var(pat)?,
_ => return Err(CompileError::MissingVariableName),
};
let collection = self.compile_expr(&for_of.right)?;
let max_iterations = self.extract_bound(&collection).unwrap_or(100);
let mut body = destructure_bindings;
self.compile_statement(&for_of.body, &mut body)?;
steps.push(PlanStep::BoundedLoop {
item_var,
collection,
max_iterations,
body,
});
},
Stmt::Block(block) => {
for stmt in &block.stmts {
self.compile_statement(stmt, steps)?;
}
},
Stmt::Return(ret) => {
let value = if let Some(arg) = &ret.arg {
self.compile_expr(arg)?
} else {
ValueExpr::Literal(JsonValue::Null)
};
steps.push(PlanStep::Return { value });
},
Stmt::Empty(_) => {},
Stmt::Try(try_stmt) => {
let mut try_steps = Vec::new();
for stmt in &try_stmt.block.stmts {
self.compile_statement(stmt, &mut try_steps)?;
}
let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
let var_name = handler.param.as_ref().map(|p| match p {
swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
_ => "error".to_string(),
});
let mut catch_stmts = Vec::new();
for stmt in &handler.body.stmts {
self.compile_statement(stmt, &mut catch_stmts)?;
}
(var_name, catch_stmts)
} else {
(None, Vec::new())
};
let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
let mut finally_stmts = Vec::new();
for stmt in &finalizer.stmts {
self.compile_statement(stmt, &mut finally_stmts)?;
}
finally_stmts
} else {
Vec::new()
};
steps.push(PlanStep::TryCatch {
try_steps,
catch_var,
catch_steps,
finally_steps,
});
},
Stmt::Continue(_) => {
steps.push(PlanStep::Continue);
},
Stmt::Break(_) => {
steps.push(PlanStep::Break);
},
Stmt::Decl(decl) => {
let msg = match decl {
Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
Decl::Class(_) => "Class declarations are not supported",
_ => "This declaration type is not supported",
};
return Err(CompileError::UnsupportedStatement(msg.into()));
},
Stmt::Switch(_) => {
return Err(CompileError::UnsupportedStatement(
"'switch' statements are not supported. Use if/else if/else instead".into(),
));
},
Stmt::Throw(_) => {
return Err(CompileError::UnsupportedStatement(
"'throw' statements are not supported. Use try/catch for error handling".into(),
));
},
Stmt::While(_) => {
return Err(CompileError::UnsupportedStatement(
"'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
));
},
Stmt::DoWhile(_) => {
return Err(CompileError::UnsupportedStatement(
"'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
));
},
Stmt::For(_) => {
return Err(CompileError::UnsupportedStatement(
"'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
));
},
Stmt::ForIn(_) => {
return Err(CompileError::UnsupportedStatement(
"'for-in' loops are not supported. Use for-of with .slice() instead".into(),
));
},
Stmt::Labeled(_) => {
return Err(CompileError::UnsupportedStatement(
"Labeled statements are not supported".into(),
));
},
Stmt::With(_) => {
return Err(CompileError::UnsupportedStatement(
"'with' statements are not supported".into(),
));
},
Stmt::Debugger(_) => {
return Err(CompileError::UnsupportedStatement(
"'debugger' statements are not supported".into(),
));
},
}
Ok(())
}
fn compile_var_init(
&mut self,
var_name: &str,
init: &Expr,
steps: &mut Vec<PlanStep>,
) -> Result<(), CompileError> {
if let Expr::Await(await_expr) = init {
if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
match extracted {
ExtractedCall::Http { method, path, body } => {
self.record_api_call(&method, &path);
steps.push(PlanStep::ApiCall {
result_var: var_name.into(),
method,
path,
body,
});
},
ExtractedCall::Sdk { operation, args } => {
steps.push(PlanStep::SdkCall {
result_var: var_name.into(),
operation,
args,
});
},
}
return Ok(());
}
#[cfg(feature = "mcp-code-mode")]
if let Some((server_id, tool_name, args)) =
self.try_extract_mcp_call(&await_expr.arg)?
{
steps.push(PlanStep::McpCall {
result_var: var_name.into(),
server_id,
tool_name,
args,
});
return Ok(());
}
if let Expr::Call(call) = await_expr.arg.as_ref() {
let inner = self.compile_call(call)?;
if let ValueExpr::PromiseAll { items } = inner {
return self.compile_promise_all(var_name, items, steps);
}
}
}
let expr = self.compile_expr(init)?;
steps.push(PlanStep::Assign {
var: var_name.into(),
expr,
});
Ok(())
}
fn compile_promise_all(
&mut self,
result_var: &str,
items: Vec<ValueExpr>,
steps: &mut Vec<PlanStep>,
) -> Result<(), CompileError> {
let mut calls = Vec::new();
let mut all_api_calls = true;
for (i, item) in items.iter().enumerate() {
match item {
ValueExpr::ApiCall { method, path, body } => {
let temp_var = format!("__promise_all_{}_{}", result_var, i);
calls.push((
temp_var,
method.clone(),
path.clone(),
body.as_ref().map(|b| *b.clone()),
));
},
_ => {
all_api_calls = false;
break;
},
}
}
if all_api_calls && !calls.is_empty() {
for (_, method, path, _) in &calls {
self.record_api_call(method, path);
}
steps.push(PlanStep::ParallelApiCalls {
result_var: result_var.into(),
calls,
});
Ok(())
} else {
Err(CompileError::UnsupportedExpression(
"Promise.all with non-API-call expressions".into(),
))
}
}
fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
match expr {
Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
Expr::Member(member) => {
let object = Box::new(self.compile_expr(&member.obj)?);
if let MemberProp::Ident(prop) = &member.prop {
let prop_name = prop.sym.to_string();
if prop_name == "length" {
return Ok(ValueExpr::ArrayMethod {
array: object,
method: ArrayMethodCall::Length,
});
}
}
match &member.prop {
MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
object,
property: ident.sym.to_string(),
}),
MemberProp::Computed(computed) => {
let index = Box::new(self.compile_expr(&computed.expr)?);
Ok(ValueExpr::ArrayIndex {
array: object,
index,
})
}
_ => Err(CompileError::UnsupportedExpression("private property".into())),
}
}
Expr::Call(call) => self.compile_call(call),
Expr::Object(obj) => {
let mut fields = Vec::new();
for prop in &obj.props {
match prop {
PropOrSpread::Prop(prop) => {
if let Prop::KeyValue(kv) = prop.as_ref() {
let key = self.prop_name_to_string(&kv.key)?;
let value = self.compile_expr(&kv.value)?;
fields.push(ObjectField::KeyValue { key, value });
} else if let Prop::Shorthand(ident) = prop.as_ref() {
let name = ident.sym.to_string();
fields.push(ObjectField::KeyValue {
key: name.clone(),
value: ValueExpr::Variable(name),
});
}
}
PropOrSpread::Spread(spread) => {
let expr = self.compile_expr(&spread.expr)?;
fields.push(ObjectField::Spread { expr });
}
}
}
Ok(ValueExpr::ObjectLiteral { fields })
}
Expr::Array(arr) => {
let mut items = Vec::new();
for elem in &arr.elems {
if let Some(elem) = elem {
if elem.spread.is_some() {
return Err(CompileError::UnsupportedExpression("spread".into()));
}
items.push(self.compile_expr(&elem.expr)?);
}
}
Ok(ValueExpr::ArrayLiteral { items })
}
Expr::Tpl(tpl) => {
let mut parts = Vec::new();
for (i, quasi) in tpl.quasis.iter().enumerate() {
let raw = quasi.raw.to_string();
if !raw.is_empty() {
parts.push(ValueExpr::Literal(JsonValue::String(raw)));
}
if i < tpl.exprs.len() {
parts.push(self.compile_expr(&tpl.exprs[i])?);
}
}
if parts.len() == 1 {
return Ok(parts.remove(0));
}
let mut result = parts.remove(0);
for part in parts {
result = ValueExpr::BinaryOp {
left: Box::new(result),
op: BinaryOperator::Concat,
right: Box::new(part),
};
}
Ok(result)
}
Expr::Bin(bin) => {
let left = Box::new(self.compile_expr(&bin.left)?);
let right = Box::new(self.compile_expr(&bin.right)?);
let op = self.compile_bin_op(bin.op)?;
Ok(ValueExpr::BinaryOp { left, op, right })
}
Expr::Unary(unary) => {
let operand = Box::new(self.compile_expr(&unary.arg)?);
let op = match unary.op {
UnaryOp::Bang => UnaryOperator::Not,
UnaryOp::Minus => UnaryOperator::Neg,
UnaryOp::TypeOf => UnaryOperator::TypeOf,
UnaryOp::Plus => UnaryOperator::Plus,
_ => return Err(CompileError::UnsupportedExpression("unary op".into())),
};
Ok(ValueExpr::UnaryOp { op, operand })
}
Expr::Cond(cond) => {
let condition = Box::new(self.compile_expr(&cond.test)?);
let consequent = Box::new(self.compile_expr(&cond.cons)?);
let alternate = Box::new(self.compile_expr(&cond.alt)?);
Ok(ValueExpr::Ternary {
condition,
consequent,
alternate,
})
}
Expr::Await(await_expr) => {
if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
return match extracted {
ExtractedCall::Http { method, path, body } => {
self.record_api_call(&method, &path);
Ok(ValueExpr::ApiCall {
method,
path,
body: body.map(Box::new),
})
}
ExtractedCall::Sdk { operation, args } => {
Ok(ValueExpr::SdkCall {
operation,
args: args.map(Box::new),
})
}
};
}
#[cfg(feature = "mcp-code-mode")]
if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
return Ok(ValueExpr::McpCall {
server_id,
tool_name,
args: args.map(Box::new),
});
}
let inner = self.compile_expr(&await_expr.arg)?;
Ok(ValueExpr::Await {
expr: Box::new(inner),
})
}
Expr::Arrow(_) => {
Err(CompileError::UnsupportedExpression(
"arrow function outside array method".into(),
))
}
Expr::Paren(paren) => self.compile_expr(&paren.expr),
Expr::OptChain(opt) => {
match opt.base.as_ref() {
OptChainBase::Member(member) => {
let object = Box::new(self.compile_expr(&member.obj)?);
if let MemberProp::Ident(ident) = &member.prop {
Ok(ValueExpr::OptionalChain {
object,
property: ident.sym.to_string(),
})
} else {
Err(CompileError::UnsupportedExpression("computed optional chain".into()))
}
}
_ => Err(CompileError::UnsupportedExpression("optional call".into())),
}
}
Expr::This(_) => Err(CompileError::UnsupportedExpression(
"'this' keyword is not supported".into(),
)),
Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
"Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
)),
Expr::Update(_) => Err(CompileError::UnsupportedExpression(
"Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
)),
Expr::New(_) => Err(CompileError::UnsupportedExpression(
"'new' keyword is not supported".into(),
)),
Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
"Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
)),
Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
"Tagged template literals are not supported. Use regular template literals instead".into(),
)),
Expr::Class(_) => Err(CompileError::UnsupportedExpression(
"Class expressions are not supported".into(),
)),
Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
"Generator yield is not supported".into(),
)),
Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
"'super' is not supported".into(),
)),
Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
"Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
)),
_ => Err(CompileError::UnsupportedExpression(
"This expression type is not supported in the JavaScript subset".into(),
)),
}
}
fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
return match extracted {
ExtractedCall::Http { method, path, body } => {
self.record_api_call(&method, &path);
Ok(ValueExpr::ApiCall {
method,
path,
body: body.map(Box::new),
})
},
ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
operation,
args: args.map(Box::new),
}),
};
}
#[cfg(feature = "mcp-code-mode")]
if let Some((server_id, tool_name, args)) =
self.try_extract_mcp_call(&Expr::Call(call.clone()))?
{
return Ok(ValueExpr::McpCall {
server_id,
tool_name,
args: args.map(Box::new),
});
}
if let Callee::Expr(callee) = &call.callee {
if let Expr::Member(member) = callee.as_ref() {
if let Expr::Ident(obj) = member.obj.as_ref() {
if obj.sym.as_ref() == "Promise" {
if let MemberProp::Ident(prop) = &member.prop {
if prop.sym.as_ref() == "all" {
if let Some(arg) = call.args.first() {
if let Expr::Array(arr) = arg.expr.as_ref() {
let mut items = Vec::new();
for elem in &arr.elems {
if let Some(elem) = elem {
items.push(self.compile_expr(&elem.expr)?);
}
}
return Ok(ValueExpr::PromiseAll { items });
}
}
}
}
}
}
}
}
if let Callee::Expr(callee) = &call.callee {
if let Expr::Member(member) = callee.as_ref() {
let array = Box::new(self.compile_expr(&member.obj)?);
if let MemberProp::Ident(method_ident) = &member.prop {
let method_name = method_ident.sym.as_ref();
match method_name {
"map" => {
let (item_var, body) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Map {
item_var,
body: Box::new(body),
},
});
},
"filter" => {
let (item_var, predicate) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Filter {
item_var,
predicate: Box::new(predicate),
},
});
},
"find" => {
let (item_var, predicate) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Find {
item_var,
predicate: Box::new(predicate),
},
});
},
"some" => {
let (item_var, predicate) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Some {
item_var,
predicate: Box::new(predicate),
},
});
},
"every" => {
let (item_var, predicate) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Every {
item_var,
predicate: Box::new(predicate),
},
});
},
"flatMap" => {
let (item_var, body) = self.extract_arrow_callback(call)?;
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::FlatMap {
item_var,
body: Box::new(body),
},
});
},
"slice" => {
let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Slice { start, end },
});
},
"push" => {
if let Some(arg) = call.args.first() {
let item = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Push { item },
});
}
},
"concat" => {
if let Some(arg) = call.args.first() {
let other = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Concat { other },
});
}
},
"includes" => {
if let Some(arg) = call.args.first() {
let item = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Includes { item },
});
}
},
"indexOf" => {
if let Some(arg) = call.args.first() {
let item = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::IndexOf { item },
});
}
},
"join" => {
let separator = if let Some(arg) = call.args.first() {
if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
Some(s.value.to_string_lossy().into_owned())
} else {
None
}
} else {
None
};
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Join { separator },
});
},
"reverse" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Reverse,
});
},
"sort" => {
let comparator = if !call.args.is_empty() {
let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
Some((a_var, b_var, Box::new(body)))
} else {
None
};
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Sort { comparator },
});
},
"flat" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Flat,
});
},
"at" => {
if let Some(n) = self.extract_number_arg(call, 0)? {
if n == 0 {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::First,
});
} else if n == -1 {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Last,
});
}
}
},
"reduce" => {
if call.args.len() >= 2 {
let (acc_var, item_var, body) =
self.extract_reduce_callback(call)?;
let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Reduce {
acc_var,
item_var,
body: Box::new(body),
initial,
},
});
}
},
"toFixed" => {
let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
return Ok(ValueExpr::NumberMethod {
number: array, method: NumberMethodCall::ToFixed { digits },
});
},
"toLowerCase" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::ToLowerCase,
});
},
"toUpperCase" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::ToUpperCase,
});
},
"startsWith" => {
let arg = call.args.first().ok_or_else(|| {
CompileError::UnsupportedExpression(
"startsWith() requires a search argument".into(),
)
})?;
let search = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::StartsWith { search },
});
},
"endsWith" => {
let arg = call.args.first().ok_or_else(|| {
CompileError::UnsupportedExpression(
"endsWith() requires a search argument".into(),
)
})?;
let search = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::EndsWith { search },
});
},
"trim" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Trim,
});
},
"replace" => {
if call.args.len() < 2 {
return Err(CompileError::UnsupportedExpression(
"replace() requires search and replacement arguments".into(),
));
}
let search = Box::new(self.compile_expr(&call.args[0].expr)?);
let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Replace {
search,
replacement,
},
});
},
"split" => {
let arg = call.args.first().ok_or_else(|| {
CompileError::UnsupportedExpression(
"split() requires a separator argument".into(),
)
})?;
let separator = Box::new(self.compile_expr(&arg.expr)?);
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Split { separator },
});
},
"substring" => {
let arg = call.args.first().ok_or_else(|| {
CompileError::UnsupportedExpression(
"substring() requires a start argument".into(),
)
})?;
let start = Box::new(self.compile_expr(&arg.expr)?);
let end = if call.args.len() >= 2 {
Some(Box::new(self.compile_expr(&call.args[1].expr)?))
} else {
None
};
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::Substring { start, end },
});
},
"toString" => {
return Ok(ValueExpr::ArrayMethod {
array,
method: ArrayMethodCall::ToString,
});
},
_ => {},
}
}
}
}
if let Callee::Expr(callee) = &call.callee {
if let Expr::Ident(ident) = callee.as_ref() {
let func = match ident.sym.as_ref() {
"parseFloat" => Some(BuiltinFunction::ParseFloat),
"parseInt" => Some(BuiltinFunction::ParseInt),
"Number" => Some(BuiltinFunction::NumberCast),
_ => None,
};
if let Some(func) = func {
let args = call
.args
.iter()
.map(|a| self.compile_expr(&a.expr))
.collect::<Result<Vec<_>, _>>()?;
return Ok(ValueExpr::BuiltinCall { func, args });
}
}
if let Expr::Member(member) = callee.as_ref() {
if let Expr::Ident(obj) = member.obj.as_ref() {
if let MemberProp::Ident(prop) = &member.prop {
let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
("Math", "abs") => Some(BuiltinFunction::MathAbs),
("Math", "max") => Some(BuiltinFunction::MathMax),
("Math", "min") => Some(BuiltinFunction::MathMin),
("Math", "round") => Some(BuiltinFunction::MathRound),
("Math", "floor") => Some(BuiltinFunction::MathFloor),
("Math", "ceil") => Some(BuiltinFunction::MathCeil),
("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
("Object", "values") => Some(BuiltinFunction::ObjectValues),
("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
_ => None,
};
if let Some(func) = func {
let args = call
.args
.iter()
.map(|a| self.compile_expr(&a.expr))
.collect::<Result<Vec<_>, _>>()?;
return Ok(ValueExpr::BuiltinCall { func, args });
}
}
}
}
}
Err(CompileError::UnsupportedExpression("function call".into()))
}
fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
let call = match expr {
Expr::Call(c) => c,
_ => return Ok(None),
};
if let Callee::Expr(callee) = &call.callee {
if let Expr::Member(member) = callee.as_ref() {
if let Expr::Ident(obj) = member.obj.as_ref() {
if obj.sym.as_ref() == "api" {
if let MemberProp::Ident(method_ident) = &member.prop {
let method_name = method_ident.sym.as_ref();
if !self.sdk_operations.is_empty() {
if !self.sdk_operations.contains(method_name) {
return Err(CompileError::InvalidApiCall(format!(
"Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
method_name
)));
}
let args = if let Some(arg) = call.args.first() {
Some(self.compile_expr(&arg.expr)?)
} else {
None
};
self.api_call_count += 1;
let op_endpoint = format!("sdk:{}", method_name);
if !self.endpoints.contains(&op_endpoint) {
self.endpoints.push(op_endpoint);
}
if !self.methods_used.contains(&method_name.to_string()) {
self.methods_used.push(method_name.to_string());
}
return Ok(Some(ExtractedCall::Sdk {
operation: method_name.to_string(),
args,
}));
}
if HttpMethod::from_str(method_name).is_none() {
return Err(CompileError::InvalidApiCall(format!(
"Unknown method: api.{}",
method_name
)));
}
let path = if let Some(arg) = call.args.first() {
self.extract_path_template(&arg.expr)?
} else {
return Err(CompileError::InvalidApiCall(
"API call requires path".into(),
));
};
let body = if let Some(arg) = call.args.get(1) {
Some(self.compile_expr(&arg.expr)?)
} else {
None
};
return Ok(Some(ExtractedCall::Http {
method: method_name.to_uppercase(),
path,
body,
}));
}
}
}
}
}
Ok(None)
}
#[cfg(feature = "mcp-code-mode")]
fn try_extract_mcp_call(
&mut self,
expr: &Expr,
) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
let call = match expr {
Expr::Call(c) => c,
_ => return Ok(None),
};
if let Callee::Expr(callee) = &call.callee {
if let Expr::Member(member) = callee.as_ref() {
if let Expr::Ident(obj) = member.obj.as_ref() {
if obj.sym.as_ref() == "mcp" {
if let MemberProp::Ident(method_ident) = &member.prop {
if method_ident.sym.as_ref() == "call" {
let server_id = call.args.first()
.and_then(|a| {
if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
Some(s.value.to_string_lossy().into_owned())
} else {
None
}
})
.ok_or_else(|| CompileError::UnsupportedExpression(
"mcp.call() first argument must be a string literal (server_id)".into(),
))?;
let tool_name = call.args.get(1)
.and_then(|a| {
if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
Some(s.value.to_string_lossy().into_owned())
} else {
None
}
})
.ok_or_else(|| CompileError::UnsupportedExpression(
"mcp.call() second argument must be a string literal (tool_name)".into(),
))?;
let args = call
.args
.get(2)
.map(|a| self.compile_expr(&a.expr))
.transpose()?;
return Ok(Some((server_id, tool_name, args)));
}
}
}
}
}
}
Ok(None)
}
fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
match expr {
Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
s.value.to_string_lossy().into_owned(),
)),
Expr::Tpl(tpl) => {
let mut parts = Vec::new();
for (i, quasi) in tpl.quasis.iter().enumerate() {
let raw = quasi.raw.to_string();
if !raw.is_empty() {
parts.push(PathPart::Literal(raw));
}
if i < tpl.exprs.len() {
if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
parts.push(PathPart::Variable(ident.sym.to_string()));
} else {
let expr = self.compile_expr(&tpl.exprs[i])?;
parts.push(PathPart::Expression(expr));
}
}
}
Ok(PathTemplate { parts })
},
_ => Err(CompileError::InvalidPath(
"Path must be a string or template literal".into(),
)),
}
}
fn extract_arrow_callback(
&mut self,
call: &CallExpr,
) -> Result<(String, ValueExpr), CompileError> {
let arg = call
.args
.first()
.ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
if let Expr::Arrow(arrow) = arg.expr.as_ref() {
let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
ident.id.sym.to_string()
} else {
return Err(CompileError::UnsupportedExpression(
"complex callback parameter".into(),
));
};
let body = match &*arrow.body {
BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
BlockStmtOrExpr::BlockStmt(block) => {
let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
let mut return_expr: Option<ValueExpr> = None;
for stmt in &block.stmts {
match stmt {
Stmt::Decl(Decl::Var(var_decl)) => {
for decl in &var_decl.decls {
let var_name = self.get_var_name(&decl.name)?;
if let Some(init) = &decl.init {
let expr = self.compile_expr(init)?;
bindings.push((var_name, expr));
}
}
},
Stmt::Return(ret) => {
if let Some(arg) = &ret.arg {
return_expr = Some(self.compile_expr(arg)?);
}
break; },
Stmt::Expr(_) => {
},
_ => {},
}
}
match return_expr {
Some(result) if bindings.is_empty() => result,
Some(result) => ValueExpr::Block {
bindings,
result: Box::new(result),
},
None => {
return Err(CompileError::UnsupportedExpression(
"callback block without return".into(),
));
},
}
},
};
Ok((param_name, body))
} else {
Err(CompileError::UnsupportedExpression(
"callback must be arrow function".into(),
))
}
}
fn extract_reduce_callback(
&mut self,
call: &CallExpr,
) -> Result<(String, String, ValueExpr), CompileError> {
let arg = call
.args
.first()
.ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
if let Expr::Arrow(arrow) = arg.expr.as_ref() {
if arrow.params.len() < 2 {
return Err(CompileError::UnsupportedExpression(
"reduce callback must have 2 parameters".into(),
));
}
let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
ident.id.sym.to_string()
} else {
return Err(CompileError::UnsupportedExpression(
"complex callback parameter".into(),
));
};
let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
ident.id.sym.to_string()
} else {
return Err(CompileError::UnsupportedExpression(
"complex callback parameter".into(),
));
};
let body = match &*arrow.body {
BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
BlockStmtOrExpr::BlockStmt(block) => {
for stmt in &block.stmts {
if let Stmt::Return(ret) = stmt {
if let Some(arg) = &ret.arg {
return Ok((acc_name, item_name, self.compile_expr(arg)?));
}
}
}
return Err(CompileError::UnsupportedExpression(
"callback block without return".into(),
));
},
};
Ok((acc_name, item_name, body))
} else {
Err(CompileError::UnsupportedExpression(
"callback must be arrow function".into(),
))
}
}
fn extract_number_arg(
&self,
call: &CallExpr,
index: usize,
) -> Result<Option<i64>, CompileError> {
if let Some(arg) = call.args.get(index) {
if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
return Ok(Some(n.value as i64));
}
if let Expr::Unary(unary) = arg.expr.as_ref() {
if unary.op == UnaryOp::Minus {
if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
return Ok(Some(-(n.value as i64)));
}
}
}
}
Ok(None)
}
fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
if let ValueExpr::ArrayMethod { method, .. } = expr {
if let ArrayMethodCall::Slice { end, .. } = method {
return *end;
}
}
None
}
fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
match pat {
Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
_ => Err(CompileError::UnsupportedExpression(
"complex destructuring".into(),
)),
}
}
fn next_temp_var(&mut self) -> String {
let name = format!("__destructure_{}", self.destructure_counter);
self.destructure_counter += 1;
name
}
fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
match pat {
Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
Pat::Object(obj_pat) => {
let temp_var = self.next_temp_var();
let bindings = Self::extract_object_bindings(obj_pat)?;
let steps = bindings
.into_iter()
.map(|(var_name, property)| PlanStep::Assign {
var: var_name,
expr: ValueExpr::PropertyAccess {
object: Box::new(ValueExpr::Variable(temp_var.clone())),
property,
},
})
.collect();
Ok((temp_var, steps))
},
Pat::Array(arr_pat) => {
let temp_var = self.next_temp_var();
let mut steps = Vec::new();
for (i, elem) in arr_pat.elems.iter().enumerate() {
if let Some(p) = elem {
let var_name = self.get_var_name(p)?;
steps.push(PlanStep::Assign {
var: var_name,
expr: ValueExpr::ArrayIndex {
array: Box::new(ValueExpr::Variable(temp_var.clone())),
index: Box::new(ValueExpr::Literal(JsonValue::Number(
(i as i64).into(),
))),
},
});
}
}
Ok((temp_var, steps))
},
_ => Err(CompileError::UnsupportedExpression(
"complex loop variable pattern".into(),
)),
}
}
fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
let mut bindings = Vec::new();
for prop in &obj_pat.props {
match prop {
ObjectPatProp::Assign(assign) => {
if assign.value.is_some() {
return Err(CompileError::UnsupportedExpression(
"default values in destructuring".into(),
));
}
let name = assign.key.sym.to_string();
bindings.push((name.clone(), name));
},
ObjectPatProp::KeyValue(kv) => {
let key = match &kv.key {
PropName::Ident(ident) => ident.sym.to_string(),
PropName::Str(s) => s.value.to_string_lossy().into_owned(),
_ => {
return Err(CompileError::UnsupportedExpression(
"computed destructuring key".into(),
));
},
};
let var_name = match kv.value.as_ref() {
Pat::Ident(ident) => ident.id.sym.to_string(),
_ => {
return Err(CompileError::UnsupportedExpression(
"nested destructuring".into(),
));
},
};
bindings.push((var_name, key));
},
ObjectPatProp::Rest(_) => {
return Err(CompileError::UnsupportedExpression(
"rest pattern in destructuring".into(),
));
},
}
}
Ok(bindings)
}
fn compile_object_destructuring(
&mut self,
obj_pat: &ObjectPat,
init: &Expr,
steps: &mut Vec<PlanStep>,
) -> Result<(), CompileError> {
let bindings = Self::extract_object_bindings(obj_pat)?;
let temp_var = self.next_temp_var();
self.compile_var_init(&temp_var, init, steps)?;
for (var_name, property) in bindings {
steps.push(PlanStep::Assign {
var: var_name,
expr: ValueExpr::PropertyAccess {
object: Box::new(ValueExpr::Variable(temp_var.clone())),
property,
},
});
}
Ok(())
}
fn compile_array_destructuring(
&mut self,
arr_pat: &ArrayPat,
init: &Expr,
steps: &mut Vec<PlanStep>,
) -> Result<(), CompileError> {
let temp_var = self.next_temp_var();
self.compile_var_init(&temp_var, init, steps)?;
for (i, elem) in arr_pat.elems.iter().enumerate() {
if let Some(pat) = elem {
let var_name = self.get_var_name(pat)?;
steps.push(PlanStep::Assign {
var: var_name,
expr: ValueExpr::ArrayIndex {
array: Box::new(ValueExpr::Variable(temp_var.clone())),
index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
},
});
}
}
Ok(())
}
fn lit_to_json(&self, lit: &Lit) -> JsonValue {
match lit {
Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
Lit::Num(n) => {
if n.value.fract() == 0.0 {
JsonValue::Number((n.value as i64).into())
} else {
serde_json::Number::from_f64(n.value)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null)
}
},
Lit::Bool(b) => JsonValue::Bool(b.value),
Lit::Null(_) => JsonValue::Null,
_ => JsonValue::Null,
}
}
fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
match prop {
PropName::Ident(ident) => Ok(ident.sym.to_string()),
PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
PropName::Num(n) => Ok(n.value.to_string()),
_ => Err(CompileError::UnsupportedExpression(
"computed property".into(),
)),
}
}
fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
match op {
BinaryOp::Add => Ok(BinaryOperator::Add),
BinaryOp::Sub => Ok(BinaryOperator::Sub),
BinaryOp::Mul => Ok(BinaryOperator::Mul),
BinaryOp::Div => Ok(BinaryOperator::Div),
BinaryOp::Mod => Ok(BinaryOperator::Mod),
BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
BinaryOp::EqEq => Ok(BinaryOperator::Eq),
BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
BinaryOp::Lt => Ok(BinaryOperator::Lt),
BinaryOp::LtEq => Ok(BinaryOperator::Lte),
BinaryOp::Gt => Ok(BinaryOperator::Gt),
BinaryOp::GtEq => Ok(BinaryOperator::Gte),
BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
BinaryOp::NullishCoalescing => {
Err(CompileError::UnsupportedExpression(
"nullish coalescing".into(),
))
},
_ => Err(CompileError::UnsupportedExpression(format!(
"binary operator {:?}",
op
))),
}
}
fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
self.api_call_count += 1;
if !self.methods_used.contains(&method.to_string()) {
self.methods_used.push(method.to_string());
}
if method != "GET" && method != "HEAD" && method != "OPTIONS" {
self.has_mutations = true;
}
let endpoint = if !path.is_dynamic() {
path.parts
.iter()
.filter_map(|p| match p {
PathPart::Literal(s) => Some(s.clone()),
_ => None,
})
.collect::<String>()
} else {
"{dynamic}".to_string()
};
if !self.endpoints.contains(&endpoint) {
self.endpoints.push(endpoint);
}
}
}
impl Default for PlanCompiler {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
pub trait HttpExecutor: Send + Sync {
async fn execute_request(
&self,
method: &str,
path: &str,
body: Option<JsonValue>,
) -> Result<JsonValue, ExecutionError>;
}
#[cfg(feature = "mcp-code-mode")]
#[async_trait::async_trait]
pub trait McpExecutor: Send + Sync {
async fn call_tool(
&self,
server_id: &str,
tool_name: &str,
args: JsonValue,
) -> Result<JsonValue, ExecutionError>;
}
#[async_trait::async_trait]
pub trait SdkExecutor: Send + Sync {
async fn execute_operation(
&self,
operation: &str,
args: Option<JsonValue>,
) -> Result<JsonValue, ExecutionError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MockExecutionMode {
#[default]
DryRun,
Testing,
Record,
}
pub struct MockHttpExecutor {
mode: MockExecutionMode,
responses: std::sync::RwLock<HashMap<String, JsonValue>>,
default_response: JsonValue,
recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MockedCall {
pub method: String,
pub path: String,
pub body: Option<JsonValue>,
pub response: JsonValue,
}
impl MockHttpExecutor {
pub fn new_dry_run() -> Self {
Self {
mode: MockExecutionMode::DryRun,
responses: std::sync::RwLock::new(HashMap::new()),
default_response: JsonValue::Object(serde_json::Map::new()),
recorded_calls: std::sync::RwLock::new(Vec::new()),
}
}
pub fn new_testing() -> Self {
Self {
mode: MockExecutionMode::Testing,
responses: std::sync::RwLock::new(HashMap::new()),
default_response: JsonValue::Object(serde_json::Map::new()),
recorded_calls: std::sync::RwLock::new(Vec::new()),
}
}
pub fn with_default_response(mut self, response: JsonValue) -> Self {
self.default_response = response;
self
}
pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
self.responses
.write()
.unwrap()
.insert(path_pattern.to_string(), response);
self
}
pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
self.responses
.write()
.unwrap()
.insert(path_pattern.to_string(), response);
}
pub fn recorded_calls(&self) -> Vec<MockedCall> {
self.recorded_calls.read().unwrap().clone()
}
pub fn clear_calls(&self) {
self.recorded_calls.write().unwrap().clear();
}
pub fn call_count(&self) -> usize {
self.recorded_calls.read().unwrap().len()
}
pub fn was_called(&self, path: &str) -> bool {
self.recorded_calls
.read()
.unwrap()
.iter()
.any(|c| c.path == path)
}
pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
self.recorded_calls
.read()
.unwrap()
.iter()
.any(|c| c.method == method && c.path == path)
}
fn find_response(&self, path: &str) -> JsonValue {
let responses = self.responses.read().unwrap();
if let Some(response) = responses.get(path) {
return response.clone();
}
for (pattern, response) in responses.iter() {
if Self::matches_pattern(pattern, path) {
return response.clone();
}
}
self.default_response.clone()
}
fn matches_pattern(pattern: &str, path: &str) -> bool {
if !pattern.contains('*') {
return pattern == path;
}
let pattern_parts: Vec<&str> = pattern.split('/').collect();
let path_parts: Vec<&str> = path.split('/').collect();
if pattern_parts.len() != path_parts.len() {
if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
} else {
return false;
}
}
for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
if *p != "*" && *p != *s {
return false;
}
}
true
}
}
#[async_trait::async_trait]
impl HttpExecutor for MockHttpExecutor {
async fn execute_request(
&self,
method: &str,
path: &str,
body: Option<JsonValue>,
) -> Result<JsonValue, ExecutionError> {
let response = self.find_response(path);
let call = MockedCall {
method: method.to_string(),
path: path.to_string(),
body,
response: response.clone(),
};
self.recorded_calls.write().unwrap().push(call);
Ok(response)
}
}
unsafe impl Send for MockHttpExecutor {}
unsafe impl Sync for MockHttpExecutor {}
#[derive(Debug, Clone, Serialize)]
pub struct ExecutionResult {
pub value: JsonValue,
pub api_calls: Vec<ApiCallLog>,
pub execution_time_ms: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ApiCallLog {
pub method: String,
pub path: String,
pub body: Option<JsonValue>,
pub response: JsonValue,
pub duration_ms: u64,
}
pub struct PlanExecutor<H: HttpExecutor> {
http: H,
config: ExecutionConfig,
variables: HashMap<String, JsonValue>,
api_calls: Vec<ApiCallLog>,
api_call_count: usize,
#[cfg(feature = "mcp-code-mode")]
mcp: Option<Box<dyn McpExecutor>>,
sdk: Option<Box<dyn SdkExecutor>>,
}
impl<H: HttpExecutor> PlanExecutor<H> {
pub fn new(http: H, config: ExecutionConfig) -> Self {
Self {
http,
config,
variables: HashMap::new(),
api_calls: Vec::new(),
api_call_count: 0,
#[cfg(feature = "mcp-code-mode")]
mcp: None,
sdk: None,
}
}
#[cfg(feature = "mcp-code-mode")]
pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
self.mcp = Some(Box::new(executor));
}
pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
self.sdk = Some(Box::new(executor));
}
pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
self.variables.insert(name.into(), value);
}
pub async fn execute(
&mut self,
plan: &ExecutionPlan,
) -> Result<ExecutionResult, ExecutionError> {
let start = std::time::Instant::now();
let mut return_value = JsonValue::Null;
for step in &plan.steps {
match self.execute_step(step).await? {
StepOutcome::Return(value) => {
return_value = value;
break; },
StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
}
}
let blocked_in_output =
find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
if !blocked_in_output.is_empty() {
return Err(ExecutionError::RuntimeError {
message: format!(
"Script output contains blocked fields: {}",
blocked_in_output.join(", ")
),
});
}
Ok(ExecutionResult {
value: return_value,
api_calls: std::mem::take(&mut self.api_calls),
execution_time_ms: start.elapsed().as_millis() as u64,
})
}
fn execute_step<'a>(
&'a mut self,
step: &'a PlanStep,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
> {
Box::pin(async move {
match step {
PlanStep::ApiCall {
result_var,
method,
path,
body,
} => {
self.api_call_count += 1;
if self.api_call_count > self.config.max_api_calls {
return Err(ExecutionError::RuntimeError {
message: format!(
"Too many API calls: {} (max: {})",
self.api_call_count, self.config.max_api_calls
),
});
}
let resolved_path = self.resolve_path(path)?;
let resolved_body = match body {
Some(expr) => Some(self.evaluate(expr)?),
None => None,
};
let call_start = std::time::Instant::now();
let raw_response = self
.http
.execute_request(method, &resolved_path, resolved_body.clone())
.await
.map_err(|e| ExecutionError::RuntimeError {
message: format!("{} {} failed: {}", method, resolved_path, e),
})?;
let duration_ms = call_start.elapsed().as_millis() as u64;
let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
self.api_calls.push(ApiCallLog {
method: method.clone(),
path: resolved_path,
body: resolved_body,
response: response.clone(),
duration_ms,
});
if result_var != "_" {
self.variables.insert(result_var.clone(), response);
}
Ok(StepOutcome::None)
},
PlanStep::Assign { var, expr } => {
let value = self.evaluate(expr)?;
self.variables.insert(var.clone(), value);
Ok(StepOutcome::None)
},
PlanStep::Conditional {
condition,
then_steps,
else_steps,
} => {
let cond_value = self.evaluate(condition)?;
let steps = if shared_is_truthy(&cond_value) {
then_steps
} else {
else_steps
};
for step in steps {
match self.execute_step(step).await? {
StepOutcome::None => {},
outcome => return Ok(outcome),
}
}
Ok(StepOutcome::None)
},
PlanStep::BoundedLoop {
item_var,
collection,
max_iterations,
body,
} => {
let collection_value = self.evaluate(collection)?;
let items = match collection_value {
JsonValue::Array(arr) => arr,
_ => {
return Err(ExecutionError::RuntimeError {
message: "Loop collection must be an array".into(),
})
},
};
let limit = (*max_iterations).min(self.config.max_loop_iterations);
'outer: for (_i, item) in items.into_iter().take(limit).enumerate() {
self.variables.insert(item_var.clone(), item);
for step in body {
match self.execute_step(step).await? {
StepOutcome::Return(value) => {
return Ok(StepOutcome::Return(value))
},
StepOutcome::None => {},
StepOutcome::Continue => continue 'outer,
StepOutcome::Break => break 'outer,
}
}
}
Ok(StepOutcome::None)
},
PlanStep::Return { value } => {
let result = self.evaluate(value)?;
Ok(StepOutcome::Return(result))
},
PlanStep::TryCatch {
try_steps,
catch_var,
catch_steps,
finally_steps,
} => {
let try_result = async {
for step in try_steps {
match self.execute_step(step).await? {
StepOutcome::None => {},
outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
}
}
Ok(StepOutcome::None)
}
.await;
let result = match try_result {
Ok(outcome) => {
outcome
},
Err(error) => {
if let Some(var) = catch_var {
let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
"message".to_string(),
JsonValue::String(format!("{}", error)),
)]));
self.variables.insert(var.clone(), error_obj);
}
let mut catch_outcome = StepOutcome::None;
for step in catch_steps {
match self.execute_step(step).await? {
StepOutcome::None => {},
outcome => {
catch_outcome = outcome;
break;
},
}
}
catch_outcome
},
};
for step in finally_steps {
match self.execute_step(step).await? {
StepOutcome::None => {},
outcome => return Ok(outcome),
}
}
Ok(result)
},
PlanStep::ParallelApiCalls { result_var, calls } => {
let mut results = Vec::with_capacity(calls.len());
for (_temp_var, method, path, body) in calls {
self.api_call_count += 1;
if self.api_call_count > self.config.max_api_calls {
return Err(ExecutionError::RuntimeError {
message: format!(
"Maximum API calls exceeded ({})",
self.config.max_api_calls
),
});
}
let resolved_path = self.resolve_path(path)?;
let resolved_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
let call_start = std::time::Instant::now();
let raw_response = self
.http
.execute_request(method, &resolved_path, resolved_body.clone())
.await
.map_err(|e| ExecutionError::RuntimeError {
message: format!("{} {} failed: {}", method, resolved_path, e),
})?;
let duration_ms = call_start.elapsed().as_millis() as u64;
let response =
filter_blocked_fields(raw_response, &self.config.blocked_fields);
self.api_calls.push(ApiCallLog {
method: method.clone(),
path: resolved_path,
body: resolved_body,
response: response.clone(),
duration_ms,
});
results.push(response);
}
self.variables
.insert(result_var.clone(), JsonValue::Array(results));
Ok(StepOutcome::None)
},
PlanStep::Continue => Ok(StepOutcome::Continue),
PlanStep::Break => Ok(StepOutcome::Break),
#[cfg(feature = "mcp-code-mode")]
PlanStep::McpCall {
result_var,
server_id,
tool_name,
args,
} => {
self.api_call_count += 1;
if self.api_call_count > self.config.max_api_calls {
return Err(ExecutionError::RuntimeError {
message: format!(
"Too many calls: {} (max: {})",
self.api_call_count, self.config.max_api_calls
),
});
}
let resolved_args = match args {
Some(expr) => self.evaluate(expr)?,
None => JsonValue::Object(Default::default()),
};
let mcp_executor =
self.mcp
.as_ref()
.ok_or_else(|| ExecutionError::RuntimeError {
message: "MCP executor not configured".into(),
})?;
let call_start = std::time::Instant::now();
let result = mcp_executor
.call_tool(server_id, tool_name, resolved_args.clone())
.await?;
let duration_ms = call_start.elapsed().as_millis() as u64;
self.api_calls.push(ApiCallLog {
method: format!("MCP:{}.{}", server_id, tool_name),
path: format!("{}/{}", server_id, tool_name),
body: Some(resolved_args),
response: result.clone(),
duration_ms,
});
if result_var != "_" {
self.variables.insert(result_var.clone(), result);
}
Ok(StepOutcome::None)
},
PlanStep::SdkCall {
result_var,
operation,
args,
} => {
self.api_call_count += 1;
if self.api_call_count > self.config.max_api_calls {
return Err(ExecutionError::RuntimeError {
message: format!(
"Too many calls: {} (max: {})",
self.api_call_count, self.config.max_api_calls
),
});
}
let resolved_args =
args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
let sdk_executor =
self.sdk
.as_ref()
.ok_or_else(|| ExecutionError::RuntimeError {
message: "SDK executor not configured".into(),
})?;
let call_start = std::time::Instant::now();
let result = sdk_executor
.execute_operation(operation, resolved_args.clone())
.await?;
let duration_ms = call_start.elapsed().as_millis() as u64;
self.api_calls.push(ApiCallLog {
method: operation.clone(),
path: format!("sdk:{}", operation),
body: resolved_args,
response: result.clone(),
duration_ms,
});
if result_var != "_" {
self.variables.insert(result_var.clone(), result);
}
Ok(StepOutcome::None)
},
}
})
}
fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
let mut result = String::new();
for part in &path.parts {
match part {
PathPart::Literal(s) => result.push_str(s),
PathPart::Variable(var) => {
let value =
self.variables
.get(var)
.ok_or_else(|| ExecutionError::RuntimeError {
message: format!("Undefined variable in path: {}", var),
})?;
result.push_str(&shared_json_to_string_with_mode(
value,
JsonStringMode::Json,
));
},
PathPart::Expression(expr) => {
let value = self.evaluate(expr)?;
result.push_str(&shared_json_to_string_with_mode(
&value,
JsonStringMode::Json,
));
},
}
}
Ok(result)
}
fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
shared_evaluate(expr, &self.variables)
}
fn evaluate_with_binding(
&self,
expr: &ValueExpr,
var: &str,
value: &JsonValue,
) -> Result<JsonValue, ExecutionError> {
shared_evaluate_with_binding(expr, &self.variables, var, value)
}
fn evaluate_with_two_bindings(
&self,
expr: &ValueExpr,
var1: &str,
value1: &JsonValue,
var2: &str,
value2: &JsonValue,
) -> Result<JsonValue, ExecutionError> {
shared_evaluate_with_two_bindings(expr, &self.variables, var1, value1, var2, value2)
}
}
pub type JsExecutor = PlanCompiler;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_config_default() {
let config = ExecutionConfig::default();
assert_eq!(config.max_api_calls, 50);
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.max_loop_iterations, 100);
}
#[test]
fn test_path_template_static() {
let path = PathTemplate::static_path("/users".into());
assert!(!path.is_dynamic());
}
#[test]
fn test_path_template_dynamic() {
let path = PathTemplate {
parts: vec![
PathPart::Literal("/users/".into()),
PathPart::Variable("id".into()),
],
};
assert!(path.is_dynamic());
}
#[test]
fn test_plan_metadata() {
let metadata = PlanMetadata {
api_call_count: 2,
has_mutations: false,
endpoints: vec!["/users".into(), "/products".into()],
methods_used: vec!["GET".into()],
};
assert_eq!(metadata.api_call_count, 2);
assert!(!metadata.has_mutations);
}
#[test]
fn test_compile_simple_api_call() {
let code = r#"
const user = await api.get('/users/1');
return user;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert_eq!(plan.metadata.api_call_count, 1);
assert!(!plan.metadata.has_mutations);
assert_eq!(plan.steps.len(), 2); }
#[test]
fn test_compile_multiple_api_calls() {
let code = r#"
const users = await api.get('/users');
const products = await api.get('/products');
return { users, products };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert_eq!(plan.metadata.api_call_count, 2);
assert!(!plan.metadata.has_mutations);
}
#[test]
fn test_compile_mutation() {
let code = r#"
const result = await api.post('/users', { name: 'Test' });
return result;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert_eq!(plan.metadata.api_call_count, 1);
assert!(plan.metadata.has_mutations);
}
#[test]
fn test_compile_dynamic_path() {
let code = r#"
const id = 123;
const user = await api.get(`/users/${id}`);
return user;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert_eq!(plan.metadata.api_call_count, 1);
}
#[test]
fn test_compile_bounded_loop() {
let code = r#"
const items = [];
const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
for (const user of users.slice(0, 2)) {
const detail = await api.get(`/users/${user.id}`);
items.push(detail);
}
return items;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert!(plan
.steps
.iter()
.any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
}
#[test]
fn test_compile_unbounded_loop_detection() {
let code = r#"
const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
for (const user of users) {
const detail = await api.get(`/users/${user.id}`);
}
return users;
"#;
let mut compiler = PlanCompiler::new();
let result = compiler.compile_code(code);
assert!(result.is_ok(), "Loop compiled: {:?}", result);
}
#[test]
fn test_compile_conditional() {
let code = r#"
const user = await api.get('/users/1');
if (user.active) {
const orders = await api.get(`/users/${user.id}/orders`);
return orders;
} else {
return [];
}
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
assert!(plan
.steps
.iter()
.any(|s| matches!(s, PlanStep::Conditional { .. })));
}
struct MockHttpExecutor {
responses: std::collections::HashMap<String, JsonValue>,
}
impl MockHttpExecutor {
fn new() -> Self {
Self {
responses: std::collections::HashMap::new(),
}
}
fn add_response(&mut self, path: &str, response: JsonValue) {
self.responses.insert(path.to_string(), response);
}
}
#[async_trait::async_trait]
impl HttpExecutor for MockHttpExecutor {
async fn execute_request(
&self,
_method: &str,
path: &str,
_body: Option<JsonValue>,
) -> Result<JsonValue, ExecutionError> {
self.responses
.get(path)
.cloned()
.ok_or_else(|| ExecutionError::RuntimeError {
message: format!("No mock response for path: {}", path),
})
}
}
#[tokio::test]
async fn test_execute_simple_api_call() {
let code = r#"
const user = await api.get('/users/1');
return user;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["id"], 1);
assert_eq!(result.value["name"], "Alice");
assert_eq!(result.api_calls.len(), 1);
}
#[tokio::test]
async fn test_execute_multiple_api_calls() {
let code = r#"
const users = await api.get('/users');
const products = await api.get('/products');
return { users, products };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
mock_http.add_response(
"/products",
serde_json::json!([{ "id": 100, "name": "Widget" }]),
);
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert!(result.value["users"].is_array());
assert!(result.value["products"].is_array());
assert_eq!(result.api_calls.len(), 2);
}
#[tokio::test]
async fn test_execute_with_template_path() {
let code = r#"
const userId = 42;
const user = await api.get(`/users/${userId}`);
return user;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["id"], 42);
assert_eq!(result.value["name"], "Bob");
}
#[tokio::test]
async fn test_execute_conditional_true_branch() {
let code = r#"
const user = await api.get('/users/1');
if (user.active) {
return { status: "active", user: user };
} else {
return { status: "inactive" };
}
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["status"], "active");
}
#[tokio::test]
async fn test_execute_conditional_false_branch() {
let code = r#"
const user = await api.get('/users/1');
if (user.active) {
return { status: "active" };
} else {
return { status: "inactive", user: user };
}
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["status"], "inactive");
}
#[tokio::test]
async fn test_compile_and_execute_reduce() {
let code = r#"
const products = await api.get('/products');
const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
return { total: totalPrice };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile reduce");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/products",
serde_json::json!([
{ "id": 1, "name": "Widget", "price": 10 },
{ "id": 2, "name": "Gadget", "price": 25 },
{ "id": 3, "name": "Gizmo", "price": 15 }
]),
);
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
}
#[tokio::test]
async fn test_compile_and_execute_to_fixed() {
let code = r#"
const products = await api.get('/products');
const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
return { averagePrice: averagePrice.toFixed(2) };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile toFixed");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/products",
serde_json::json!([
{ "id": 1, "name": "Widget", "price": 10 },
{ "id": 2, "name": "Gadget", "price": 25 },
{ "id": 3, "name": "Gizmo", "price": 15 }
]),
);
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["averagePrice"], "16.67");
}
#[test]
fn test_filter_blocked_fields_simple() {
let value = serde_json::json!({
"id": 1,
"name": "Alice",
"password": "secret123",
"email": "alice@example.com"
});
let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
let filtered = filter_blocked_fields(value, &blocked);
assert_eq!(filtered["id"], 1);
assert_eq!(filtered["name"], "Alice");
assert_eq!(filtered["email"], "alice@example.com");
assert!(filtered.get("password").is_none());
}
#[test]
fn test_filter_blocked_fields_multiple() {
let value = serde_json::json!({
"id": 1,
"name": "Alice",
"password": "secret123",
"ssn": "123-45-6789",
"apiKey": "key-abc123"
});
let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
.iter()
.map(|s| s.to_string())
.collect();
let filtered = filter_blocked_fields(value, &blocked);
assert_eq!(filtered["id"], 1);
assert_eq!(filtered["name"], "Alice");
assert!(filtered.get("password").is_none());
assert!(filtered.get("ssn").is_none());
assert!(filtered.get("apiKey").is_none());
}
#[test]
fn test_filter_blocked_fields_nested() {
let value = serde_json::json!({
"user": {
"id": 1,
"profile": {
"name": "Alice",
"password": "secret123"
}
}
});
let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
let filtered = filter_blocked_fields(value, &blocked);
assert_eq!(filtered["user"]["id"], 1);
assert_eq!(filtered["user"]["profile"]["name"], "Alice");
assert!(filtered["user"]["profile"].get("password").is_none());
}
#[test]
fn test_filter_blocked_fields_in_array() {
let value = serde_json::json!([
{ "id": 1, "name": "Alice", "password": "secret1" },
{ "id": 2, "name": "Bob", "password": "secret2" }
]);
let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
let filtered = filter_blocked_fields(value, &blocked);
let arr = filtered.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["id"], 1);
assert_eq!(arr[0]["name"], "Alice");
assert!(arr[0].get("password").is_none());
assert_eq!(arr[1]["id"], 2);
assert_eq!(arr[1]["name"], "Bob");
assert!(arr[1].get("password").is_none());
}
#[test]
fn test_filter_blocked_fields_empty_blocklist() {
let value = serde_json::json!({
"id": 1,
"password": "secret123"
});
let blocked: HashSet<String> = HashSet::new();
let filtered = filter_blocked_fields(value.clone(), &blocked);
assert_eq!(filtered, value);
}
#[test]
fn test_filter_blocked_fields_primitive_values() {
let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
assert_eq!(
filter_blocked_fields(JsonValue::String("test".into()), &blocked),
JsonValue::String("test".into())
);
assert_eq!(
filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
JsonValue::Number(42.into())
);
assert_eq!(
filter_blocked_fields(JsonValue::Bool(true), &blocked),
JsonValue::Bool(true)
);
assert_eq!(
filter_blocked_fields(JsonValue::Null, &blocked),
JsonValue::Null
);
}
#[tokio::test]
async fn test_execute_with_blocked_fields() {
let code = r#"
const user = await api.get('/users/1');
return user;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/users/1",
serde_json::json!({
"id": 1,
"name": "Alice",
"password": "secret123",
"apiKey": "key-abc"
}),
);
let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
let mut executor = PlanExecutor::new(mock_http, config);
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["id"], 1);
assert_eq!(result.value["name"], "Alice");
assert!(result.value.get("password").is_none());
assert!(result.value.get("apiKey").is_none());
}
#[tokio::test]
async fn test_execute_nested_blocked_fields() {
let code = r#"
const data = await api.get('/data');
return data;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/data",
serde_json::json!({
"users": [
{ "id": 1, "name": "Alice", "secret": "hidden1" },
{ "id": 2, "name": "Bob", "secret": "hidden2" }
],
"config": {
"setting": "value",
"secret": "also-hidden"
}
}),
);
let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
let mut executor = PlanExecutor::new(mock_http, config);
let result = executor.execute(&plan).await.expect("Should execute");
let users = result.value["users"].as_array().unwrap();
assert_eq!(users[0]["name"], "Alice");
assert!(users[0].get("secret").is_none());
assert_eq!(users[1]["name"], "Bob");
assert!(users[1].get("secret").is_none());
assert_eq!(result.value["config"]["setting"], "value");
assert!(result.value["config"].get("secret").is_none());
}
#[test]
fn test_find_blocked_fields_in_output_simple() {
let value = serde_json::json!({
"id": 1,
"name": "Alice",
"ssn": "123-45-6789"
});
let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
let violations = find_blocked_fields_in_output(&value, &blocked);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0], "ssn");
}
#[test]
fn test_find_blocked_fields_in_output_nested() {
let value = serde_json::json!({
"user": {
"profile": {
"name": "Alice",
"salary": 100000
}
}
});
let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
let violations = find_blocked_fields_in_output(&value, &blocked);
assert_eq!(violations.len(), 1);
assert!(violations[0].contains("salary"));
}
#[test]
fn test_find_blocked_fields_in_output_array() {
let value = serde_json::json!([
{ "id": 1, "ssn": "111" },
{ "id": 2, "ssn": "222" }
]);
let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
let violations = find_blocked_fields_in_output(&value, &blocked);
assert_eq!(violations.len(), 2);
}
#[test]
fn test_find_blocked_fields_in_output_empty_blocklist() {
let value = serde_json::json!({
"id": 1,
"ssn": "123-45-6789"
});
let blocked: HashSet<String> = HashSet::new();
let violations = find_blocked_fields_in_output(&value, &blocked);
assert!(violations.is_empty());
}
#[test]
fn test_find_blocked_fields_in_output_no_violations() {
let value = serde_json::json!({
"id": 1,
"name": "Alice"
});
let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
let violations = find_blocked_fields_in_output(&value, &blocked);
assert!(violations.is_empty());
}
#[tokio::test]
async fn test_execute_output_blocked_fields_rejected() {
let code = r#"
const user = await api.get('/users/1');
return { name: user.name, ssn: user.ssn };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/users/1",
serde_json::json!({
"id": 1,
"name": "Alice",
"ssn": "123-45-6789"
}),
);
let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
let mut executor = PlanExecutor::new(mock_http, config);
let result = executor.execute(&plan).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{:?}", err).contains("ssn"));
}
#[tokio::test]
async fn test_execute_output_blocked_fields_internal_use_allowed() {
let code = r#"
const user = await api.get('/users/1');
return { id: user.id, name: user.name };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/users/1",
serde_json::json!({
"id": 1,
"name": "Alice",
"ssn": "123-45-6789"
}),
);
let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
let mut executor = PlanExecutor::new(mock_http, config);
let result = executor.execute(&plan).await.expect("Should succeed");
assert_eq!(result.value["id"], 1);
assert_eq!(result.value["name"], "Alice");
assert!(result.value.get("ssn").is_none());
}
#[tokio::test]
async fn test_execute_both_blocklists() {
let code = r#"
const user = await api.get('/users/1');
return { name: user.name, dateOfBirth: user.dateOfBirth };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/users/1",
serde_json::json!({
"id": 1,
"name": "Alice",
"password": "secret123",
"dateOfBirth": "1990-01-01"
}),
);
let config = ExecutionConfig::default()
.with_blocked_fields(["password"])
.with_output_blocked_fields(["dateOfBirth"]);
let mut executor = PlanExecutor::new(mock_http, config);
let result = executor.execute(&plan).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{:?}", err).contains("dateOfBirth"));
}
#[tokio::test]
async fn test_prebound_args_comparison() {
let code = r#"
if (args.k > args.n) {
return { error: 'k must be <= n' };
}
return { ok: true };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(
result.value["error"], "k must be <= n",
"Expected error for k > n, got: {:?}",
result.value
);
}
#[tokio::test]
async fn test_prebound_args_strict_equality() {
let code = r#"
if (args.k === 0) {
return { result: 1 };
}
return { result: 'not zero' };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_variable("args", serde_json::json!({"k": 0}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["result"], 1);
}
#[tokio::test]
async fn test_assignment_expression_in_statement() {
let code = r#"
let k = 5;
k = 2;
return { k: k };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["k"], 2);
}
#[tokio::test]
async fn test_assignment_swap_variables() {
let code = r#"
let a = 3;
let b = 7;
if (a < b) {
const old_a = a;
a = b;
b = old_a;
}
return { a: a, b: b };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["a"], 7);
assert_eq!(result.value["b"], 3);
}
#[cfg(feature = "mcp-code-mode")]
mod mcp_tests {
use super::*;
struct MockCalculatorExecutor;
#[async_trait::async_trait]
impl McpExecutor for MockCalculatorExecutor {
async fn call_tool(
&self,
_server_id: &str,
tool_name: &str,
args: JsonValue,
) -> Result<JsonValue, ExecutionError> {
match tool_name {
"add" => {
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!({"result": a + b}))
},
"subtract" => {
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!({"result": a - b}))
},
"multiply" => {
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!({"result": a * b}))
},
"divide" => {
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(1.0);
Ok(serde_json::json!({"result": a / b}))
},
"power" => {
let base = args["base"].as_f64().unwrap_or(0.0);
let exponent = args["exponent"].as_f64().unwrap_or(1.0);
Ok(serde_json::json!({"result": base.powf(exponent)}))
},
"sqrt" => {
let n = args["n"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!({"result": n.sqrt()}))
},
_ => Err(ExecutionError::RuntimeError {
message: format!("Unknown tool: {}", tool_name),
}),
}
}
}
#[tokio::test]
async fn test_mcp_call_simple() {
let code = r#"
const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
return result;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["result"], 8.0);
}
#[tokio::test]
async fn test_mcp_call_with_args() {
let code = r#"
const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
return { sum: result.result };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["sum"], 30.0);
}
#[tokio::test]
async fn test_mcp_assignment_in_loop() {
let code = r#"
let result = { result: 1 };
for (const i of [2, 3, 4, 5]) {
const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
result = mul;
}
return { factorial: result.result };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["factorial"], 120.0);
}
#[tokio::test]
async fn test_combinations_c_5_3() {
let code = r#"
if (args.k > args.n) {
return { error: 'k must be <= n', n: args.n, k: args.k };
}
if (args.k === 0 || args.k === args.n) {
return { n: args.n, k: args.k, result: 1 };
}
let k = args.k;
const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
let nmk = complement.result;
if (nmk < k) {
const old_k = k;
k = nmk;
nmk = old_k;
}
let result = { result: 1 };
for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
if (i > k) { break; }
const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
}
return { n: args.n, k: args.k, result: result.result };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(
result.value["result"], 10.0,
"C(5,3) should be 10, got: {:?}",
result.value
);
}
#[tokio::test]
async fn test_combinations_k_greater_than_n() {
let code = r#"
if (args.k > args.n) {
return { error: 'k must be <= n', n: args.n, k: args.k };
}
return { result: 'should not reach here' };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(
result.value["error"], "k must be <= n",
"C(3,5) should return error, got: {:?}",
result.value
);
}
#[tokio::test]
async fn test_combinations_c_5_2() {
let code = r#"
if (args.k > args.n) {
return { error: 'k must be <= n', n: args.n, k: args.k };
}
if (args.k === 0 || args.k === args.n) {
return { n: args.n, k: args.k, result: 1 };
}
let k = args.k;
const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
let nmk = complement.result;
if (nmk < k) {
const old_k = k;
k = nmk;
nmk = old_k;
}
let result = { result: 1 };
for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
if (i > k) { break; }
const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
}
return { n: args.n, k: args.k, result: result.result };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(
result.value["result"], 10.0,
"C(5,2) should be 10, got: {:?}",
result.value
);
}
#[tokio::test]
async fn test_combinations_edge_cases() {
let code = r#"
if (args.k === 0 || args.k === args.n) {
return { result: 1 };
}
return { result: 'not edge case' };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
}
#[tokio::test]
async fn test_solve_quadratic() {
let code = r#"
const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
const root_type = discriminant.result > 0 ? 'two_real'
: discriminant.result === 0 ? 'one_real' : 'complex';
if (discriminant.result < 0) {
return { discriminant: discriminant.result, root_type: root_type, roots: [] };
}
const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
executor.set_mcp_executor(MockCalculatorExecutor);
executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["root_type"], "two_real");
assert_eq!(result.value["discriminant"], 1.0);
let roots = result.value["roots"]
.as_array()
.expect("roots should be array");
assert_eq!(roots.len(), 2);
assert_eq!(roots[0], 2.0);
assert_eq!(roots[1], 1.0);
}
}
#[tokio::test]
async fn test_string_includes() {
let code = r#"
const text = "hello world";
return { found: text.includes("world"), miss: text.includes("xyz") };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["found"], true);
assert_eq!(result.value["miss"], false);
}
#[tokio::test]
async fn test_string_index_of() {
let code = r#"
const text = "abcdef";
return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["idx"], 2);
assert_eq!(result.value["miss"], -1);
}
#[tokio::test]
async fn test_string_length() {
let code = r#"
const text = "hello";
return { len: text.length };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["len"], 5);
}
#[tokio::test]
async fn test_string_slice() {
let code = r#"
const text = "hello world";
return { first: text.slice(0, 5), rest: text.slice(6, 11) };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["first"], "hello");
assert_eq!(result.value["rest"], "world");
}
#[tokio::test]
async fn test_string_concat() {
let code = r#"
const greeting = "hello";
return { result: greeting.concat(" world") };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["result"], "hello world");
}
#[tokio::test]
async fn test_string_includes_in_filter() {
let code = r#"
const items = [
{ name: "TIMESTAMP_2024", desc: "A timestamped record" },
{ name: "PERSON_1", desc: "A person entity" },
{ name: "TIMESTAMP_2025", desc: "Another timestamped record" }
];
const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
return { count: timestamped.length, names: timestamped.map(t => t.name) };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["count"], 2);
let names = result.value["names"].as_array().unwrap();
assert_eq!(names[0], "TIMESTAMP_2024");
assert_eq!(names[1], "TIMESTAMP_2025");
}
#[tokio::test]
async fn test_array_includes_still_works() {
let code = r#"
const ids = ["alice", "bob", "charlie"];
return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).expect("Should compile");
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.expect("Should execute");
assert_eq!(result.value["has_bob"], true);
assert_eq!(result.value["has_dave"], false);
}
#[test]
fn test_compile_parse_float() {
let code = r#"
const x = parseFloat("3.14");
return x;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler
.compile_code(code)
.expect("parseFloat should compile");
assert_eq!(plan.steps.len(), 2); }
#[test]
fn test_compile_parse_int() {
let code = r#"
const x = parseInt("42");
return x;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("parseInt should compile");
}
#[test]
fn test_compile_math_abs() {
let code = r#"
const x = Math.abs(-5);
return x;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("Math.abs should compile");
}
#[test]
fn test_compile_math_max() {
let code = r#"
const x = Math.max(1, 2, 3);
return x;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("Math.max should compile");
}
#[test]
fn test_compile_object_keys() {
let code = r#"
const obj = { a: 1, b: 2 };
const keys = Object.keys(obj);
return keys;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("Object.keys should compile");
}
#[test]
fn test_compile_object_entries() {
let code = r#"
const obj = { x: 10 };
const entries = Object.entries(obj);
return entries;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("Object.entries should compile");
}
#[test]
fn test_compile_unary_plus() {
let code = r#"
const x = +"42";
return x;
"#;
let mut compiler = PlanCompiler::new();
compiler.compile_code(code).expect("unary + should compile");
}
#[test]
fn test_compile_sort_with_comparator() {
let code = r#"
const arr = [3, 1, 2];
const sorted = arr.sort((a, b) => a - b);
return sorted;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("sort with comparator should compile");
}
#[test]
fn test_compile_sort_without_comparator() {
let code = r#"
const arr = ["b", "a", "c"];
const sorted = arr.sort();
return sorted;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("sort without comparator should compile");
}
#[tokio::test]
async fn test_execute_parse_float() {
let code = r#"
const x = parseFloat("3.14");
return x;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value, serde_json::json!(3.14));
}
#[tokio::test]
async fn test_execute_math_abs_and_sort() {
let code = r#"
const items = [
{ name: "a", val: -5 },
{ name: "b", val: 3 },
{ name: "c", val: -1 }
];
const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
return sorted.map(x => x.name);
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
}
#[tokio::test]
async fn test_execute_object_keys() {
let code = r#"
const obj = { x: 1, y: 2, z: 3 };
return Object.keys(obj).length;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value, serde_json::json!(3));
}
#[tokio::test]
async fn test_execute_unary_plus() {
let code = r#"
const x = +"42";
return x;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value, serde_json::json!(42.0));
}
#[tokio::test]
async fn test_execute_number_cast() {
let code = r#"
const x = Number("99.5");
return x;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value, serde_json::json!(99.5));
}
#[tokio::test]
async fn test_execute_math_round_floor_ceil() {
let code = r#"
return {
round: Math.round(3.7),
floor: Math.floor(3.7),
ceil: Math.ceil(3.2)
};
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["round"], serde_json::json!(4.0));
assert_eq!(result.value["floor"], serde_json::json!(3.0));
assert_eq!(result.value["ceil"], serde_json::json!(4.0));
}
#[test]
fn test_compile_object_spread_basic() {
let code = r#"
const base = { id: 1, name: "Alice" };
const extended = { ...base, age: 30 };
return extended;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler
.compile_code(code)
.expect("Object spread should compile");
assert!(plan.steps.len() >= 2);
}
#[tokio::test]
async fn test_execute_object_spread_basic() {
let code = r#"
const base = { id: 1, name: "Alice" };
const extended = { ...base, age: 30 };
return extended;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["id"], serde_json::json!(1));
assert_eq!(result.value["name"], serde_json::json!("Alice"));
assert_eq!(result.value["age"], serde_json::json!(30));
}
#[tokio::test]
async fn test_execute_object_spread_override() {
let code = r#"
const obj = { id: 1, name: "old" };
const updated = { ...obj, name: "new" };
return updated;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["id"], serde_json::json!(1));
assert_eq!(result.value["name"], serde_json::json!("new"));
}
#[tokio::test]
async fn test_execute_object_spread_multiple() {
let code = r#"
const a = { x: 1 };
const b = { y: 2 };
const merged = { ...a, ...b, z: 3 };
return merged;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["x"], serde_json::json!(1));
assert_eq!(result.value["y"], serde_json::json!(2));
assert_eq!(result.value["z"], serde_json::json!(3));
}
#[tokio::test]
async fn test_execute_object_spread_with_api_result() {
let code = r#"
const config = await api.get('/config');
const result = { ...config, extra: "added" };
return result;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/config",
serde_json::json!({ "key": "value", "enabled": true }),
);
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["key"], serde_json::json!("value"));
assert_eq!(result.value["enabled"], serde_json::json!(true));
assert_eq!(result.value["extra"], serde_json::json!("added"));
}
#[tokio::test]
async fn test_execute_object_spread_non_object_noop() {
let code = r#"
const x = 42;
const obj = { ...x, name: "test" };
return obj;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["name"], serde_json::json!("test"));
assert!(result.value.as_object().unwrap().len() == 1);
}
#[tokio::test]
async fn test_execute_object_spread_preserves_order() {
let code = r#"
const obj = { a: 1, b: 2 };
const result = { b: 99, ...obj, a: 100 };
return result;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["a"], serde_json::json!(100));
assert_eq!(result.value["b"], serde_json::json!(2));
}
#[test]
fn test_compile_object_destructuring_simple() {
let code = r#"
const obj = { id: 1, name: "Alice" };
const { id, name } = obj;
return { id, name };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler
.compile_code(code)
.expect("Object destructuring should compile");
assert!(plan.steps.len() >= 4);
}
#[tokio::test]
async fn test_execute_object_destructuring_simple() {
let code = r#"
const obj = { id: 1, name: "Alice", extra: "ignored" };
const { id, name } = obj;
return { id, name };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["id"], serde_json::json!(1));
assert_eq!(result.value["name"], serde_json::json!("Alice"));
assert!(result.value.get("extra").is_none());
}
#[tokio::test]
async fn test_execute_object_destructuring_renamed() {
let code = r#"
const user = { id: 1, name: "Alice" };
const { id: userId, name: userName } = user;
return { userId, userName };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["userId"], serde_json::json!(1));
assert_eq!(result.value["userName"], serde_json::json!("Alice"));
}
#[tokio::test]
async fn test_execute_object_destructuring_with_api_call() {
let code = r#"
const { data, status } = await api.get('/users');
return { data, status };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/users",
serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
);
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
assert_eq!(result.value["status"], serde_json::json!("ok"));
}
#[tokio::test]
async fn test_execute_object_destructuring_missing_property() {
let code = r#"
const obj = { id: 1 };
const { id, name } = obj;
return { id, name };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["id"], serde_json::json!(1));
assert_eq!(result.value["name"], serde_json::json!(null));
}
#[tokio::test]
async fn test_execute_array_destructuring_simple() {
let code = r#"
const arr = [10, 20, 30];
const [a, b] = arr;
return { a, b };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["a"], serde_json::json!(10));
assert_eq!(result.value["b"], serde_json::json!(20));
}
#[tokio::test]
async fn test_execute_array_destructuring_with_promise_all() {
let code = r#"
const [users, products] = await Promise.all([
api.get('/users'),
api.get('/products')
]);
return { users, products };
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
}
#[test]
fn test_compile_for_of_destructuring() {
let code = r#"
const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
const results = [];
for (const { id, name } of items.slice(0, 10)) {
results.push({ id, name });
}
return results;
"#;
let mut compiler = PlanCompiler::new();
compiler
.compile_code(code)
.expect("For-of with destructuring should compile");
}
#[tokio::test]
async fn test_execute_for_of_destructuring() {
let code = r#"
const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
const results = [];
for (const { id, name } of items.slice(0, 10)) {
results.push({ label: name, num: id });
}
return results;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mock_http = MockHttpExecutor::new();
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
let arr = result.value.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["label"], serde_json::json!("A"));
assert_eq!(arr[0]["num"], serde_json::json!(1));
assert_eq!(arr[1]["label"], serde_json::json!("B"));
assert_eq!(arr[1]["num"], serde_json::json!(2));
}
#[tokio::test]
async fn test_execute_for_of_destructuring_with_api_calls() {
let code = r#"
const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
const results = [];
for (const { id, role } of users.slice(0, 10)) {
const detail = await api.get(`/users/${id}`);
results.push({ role, detail });
}
return results;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
let arr = result.value.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["role"], serde_json::json!("admin"));
assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
assert_eq!(arr[1]["role"], serde_json::json!("user"));
assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
}
#[tokio::test]
async fn test_execute_spread_and_destructuring_combined() {
let code = r#"
const { data, token } = await api.get('/auth');
const result = await api.post('/action', { ...data, token });
return result;
"#;
let mut compiler = PlanCompiler::new();
let plan = compiler.compile_code(code).unwrap();
let mut mock_http = MockHttpExecutor::new();
mock_http.add_response(
"/auth",
serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
);
mock_http.add_response("/action", serde_json::json!({ "success": true }));
let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
let result = executor.execute(&plan).await.unwrap();
assert_eq!(result.value["success"], serde_json::json!(true));
}
}