Skip to main content

pmcp_code_mode/
executor.rs

1//! AST-based JavaScript execution for Code Mode.
2//!
3//! This module provides secure execution of validated JavaScript code by:
4//! 1. Compiling the SWC AST to an ExecutionPlan
5//! 2. Executing the plan in pure Rust (no JS runtime)
6//!
7//! ## Security Model
8//!
9//! Only operations that can be represented in the ExecutionPlan are allowed.
10//! The plan is a simple tree structure that's fully auditable before execution.
11//!
12//! ## Supported Operations
13//!
14//! - API calls: `api.get()`, `api.post()`, `api.put()`, `api.delete()`, `api.patch()`
15//! - Variable assignment: `const x = ...`
16//! - Property access: `user.id`, `response.data`
17//! - Array methods: `.map()`, `.filter()`, `.slice()`, `.find()`, `.length`
18//! - Template literals: `` `/users/${id}` ``
19//! - Object literals: `{ name: "test", id: 123 }`
20//! - Array literals: `[1, 2, 3]`
21//! - Conditionals: `if/else`
22//! - Bounded loops: `for (const x of arr.slice(0, N))`
23//! - Return statements
24
25use crate::javascript::HttpMethod;
26use crate::types::ExecutionError;
27use serde::Serialize;
28use serde_json::Value as JsonValue;
29use std::collections::{HashMap, HashSet};
30
31// Import shared evaluation functions
32use crate::eval::{
33    evaluate as shared_evaluate, is_truthy as shared_is_truthy,
34    json_to_string_with_mode as shared_json_to_string_with_mode, JsonStringMode,
35};
36use swc_common::{FileName, SourceMap};
37use swc_ecma_ast::*;
38use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
39
40/// Internal control flow outcome from executing a single plan step.
41///
42/// Replaces the previous pattern of abusing `ExecutionError::LoopContinue`
43/// and `ExecutionError::LoopBreak` for non-error control flow.
44pub(crate) enum StepOutcome {
45    /// Step completed, no value produced.
46    None,
47    /// Step produced a return value (exits function/plan).
48    Return(serde_json::Value),
49    /// Loop continue signal (skip to next iteration).
50    Continue,
51    /// Loop break signal (exit current loop).
52    Break,
53}
54
55/// Configuration for execution.
56#[derive(Debug, Clone)]
57pub struct ExecutionConfig {
58    /// Maximum number of API calls allowed
59    pub max_api_calls: usize,
60    /// Maximum execution time in seconds
61    pub timeout_seconds: u64,
62    /// Maximum loop iterations
63    pub max_loop_iterations: usize,
64    /// Fields that should be filtered from API responses (internal blocklist).
65    /// These fields are stripped from responses before scripts can access them.
66    /// Field names are case-sensitive and matched at any nesting level.
67    pub blocked_fields: HashSet<String>,
68    /// Fields that cannot appear in script output (output blocklist).
69    /// These fields can be used internally but cannot be returned by the script.
70    /// Field names are case-sensitive and matched at any nesting level.
71    pub output_blocked_fields: HashSet<String>,
72}
73
74impl Default for ExecutionConfig {
75    fn default() -> Self {
76        Self {
77            max_api_calls: 50,
78            timeout_seconds: 30,
79            max_loop_iterations: 100,
80            blocked_fields: HashSet::new(),
81            output_blocked_fields: HashSet::new(),
82        }
83    }
84}
85
86impl ExecutionConfig {
87    /// Create a new config with blocked fields for API response filtering.
88    pub fn with_blocked_fields(
89        mut self,
90        fields: impl IntoIterator<Item = impl Into<String>>,
91    ) -> Self {
92        self.blocked_fields = fields.into_iter().map(Into::into).collect();
93        self
94    }
95
96    /// Create a new config with output blocked fields for return value validation.
97    pub fn with_output_blocked_fields(
98        mut self,
99        fields: impl IntoIterator<Item = impl Into<String>>,
100    ) -> Self {
101        self.output_blocked_fields = fields.into_iter().map(Into::into).collect();
102        self
103    }
104}
105
106/// Recursively filter blocked fields from a JSON value.
107///
108/// This removes any fields whose names are in the blocklist from objects,
109/// and recursively processes nested objects and arrays.
110///
111/// # Arguments
112///
113/// * `value` - The JSON value to filter
114/// * `blocked_fields` - Set of field names to remove
115///
116/// # Returns
117///
118/// A new JSON value with blocked fields removed.
119pub fn filter_blocked_fields(value: JsonValue, blocked_fields: &HashSet<String>) -> JsonValue {
120    if blocked_fields.is_empty() {
121        return value;
122    }
123
124    match value {
125        JsonValue::Object(mut map) => {
126            // Remove blocked fields at this level
127            map.retain(|key, _| !blocked_fields.contains(key));
128
129            // Recursively filter remaining values
130            let filtered: serde_json::Map<String, JsonValue> = map
131                .into_iter()
132                .map(|(k, v)| (k, filter_blocked_fields(v, blocked_fields)))
133                .collect();
134
135            JsonValue::Object(filtered)
136        },
137        JsonValue::Array(arr) => {
138            // Recursively filter each element
139            let filtered: Vec<JsonValue> = arr
140                .into_iter()
141                .map(|v| filter_blocked_fields(v, blocked_fields))
142                .collect();
143
144            JsonValue::Array(filtered)
145        },
146        // Non-container values pass through unchanged
147        other => other,
148    }
149}
150
151/// Find blocked fields that appear in a JSON value (output validation).
152///
153/// Unlike `filter_blocked_fields` which silently removes fields, this function
154/// identifies blocked fields without modifying the value. Used for output
155/// blocklist validation where blocked fields can be used internally but
156/// cannot appear in script output.
157///
158/// # Arguments
159///
160/// * `value` - The JSON value to check
161/// * `blocked_fields` - Set of field names that are not allowed in output
162///
163/// # Returns
164///
165/// A vector of blocked field names found in the value, along with their paths.
166pub fn find_blocked_fields_in_output(
167    value: &JsonValue,
168    blocked_fields: &HashSet<String>,
169) -> Vec<String> {
170    if blocked_fields.is_empty() {
171        return Vec::new();
172    }
173
174    let mut violations = Vec::new();
175    find_blocked_fields_recursive(value, blocked_fields, "", &mut violations);
176    violations
177}
178
179/// Recursively find blocked fields in a JSON value.
180fn find_blocked_fields_recursive(
181    value: &JsonValue,
182    blocked_fields: &HashSet<String>,
183    path: &str,
184    violations: &mut Vec<String>,
185) {
186    match value {
187        JsonValue::Object(map) => visit_object(map, blocked_fields, path, violations),
188        JsonValue::Array(arr) => visit_array(arr, blocked_fields, path, violations),
189        // Non-container values don't need checking.
190        _ => {},
191    }
192}
193
194/// Walk a JSON object: record direct blocked-key matches, then recurse into
195/// each value with an extended dotted path.
196fn visit_object(
197    map: &serde_json::Map<String, JsonValue>,
198    blocked_fields: &HashSet<String>,
199    path: &str,
200    violations: &mut Vec<String>,
201) {
202    for (key, v) in map {
203        let key_path = join_field_path(path, key);
204        if blocked_fields.contains(key) {
205            violations.push(key_path.clone());
206        }
207        find_blocked_fields_recursive(v, blocked_fields, &key_path, violations);
208    }
209}
210
211/// Walk a JSON array: recurse into each element with an indexed path.
212fn visit_array(
213    arr: &[JsonValue],
214    blocked_fields: &HashSet<String>,
215    path: &str,
216    violations: &mut Vec<String>,
217) {
218    for (i, v) in arr.iter().enumerate() {
219        let elem_path = join_index_path(path, i);
220        find_blocked_fields_recursive(v, blocked_fields, &elem_path, violations);
221    }
222}
223
224/// Append a dotted field segment to a path: `""` + `"k"` → `"k"`, `"a.b"` + `"k"` → `"a.b.k"`.
225fn join_field_path(path: &str, key: &str) -> String {
226    if path.is_empty() {
227        key.to_string()
228    } else {
229        format!("{}.{}", path, key)
230    }
231}
232
233/// Append an indexed segment to a path: `""` + `0` → `"[0]"`, `"a"` + `1` → `"a[1]"`.
234fn join_index_path(path: &str, idx: usize) -> String {
235    if path.is_empty() {
236        format!("[{}]", idx)
237    } else {
238        format!("{}[{}]", path, idx)
239    }
240}
241
242/// An execution plan compiled from JavaScript AST.
243#[derive(Debug, Clone, Serialize)]
244pub struct ExecutionPlan {
245    /// The steps to execute
246    pub steps: Vec<PlanStep>,
247    /// Metadata about the plan
248    pub metadata: PlanMetadata,
249}
250
251/// Metadata about the execution plan.
252#[derive(Debug, Clone, Serialize)]
253pub struct PlanMetadata {
254    /// Total number of API calls in the plan
255    pub api_call_count: usize,
256    /// Whether any mutations (POST/PUT/DELETE/PATCH) are present
257    pub has_mutations: bool,
258    /// List of all endpoints accessed
259    pub endpoints: Vec<String>,
260    /// HTTP methods used
261    pub methods_used: Vec<String>,
262}
263
264/// A single step in the execution plan.
265#[derive(Debug, Clone, Serialize)]
266pub enum PlanStep {
267    /// API call: `const result = await api.get('/path')`
268    ApiCall {
269        result_var: String,
270        method: String,
271        path: PathTemplate,
272        body: Option<ValueExpr>,
273    },
274
275    /// Variable assignment: `const x = expr`
276    Assign { var: String, expr: ValueExpr },
277
278    /// Conditional: `if (cond) { ... } else { ... }`
279    Conditional {
280        condition: ValueExpr,
281        then_steps: Vec<PlanStep>,
282        else_steps: Vec<PlanStep>,
283    },
284
285    /// Bounded loop: `for (const x of arr.slice(0, N)) { ... }`
286    BoundedLoop {
287        item_var: String,
288        collection: ValueExpr,
289        max_iterations: usize,
290        body: Vec<PlanStep>,
291    },
292
293    /// Return statement
294    Return { value: ValueExpr },
295
296    /// Try/catch statement: `try { ... } catch (e) { ... }`
297    TryCatch {
298        try_steps: Vec<PlanStep>,
299        catch_var: Option<String>,
300        catch_steps: Vec<PlanStep>,
301        finally_steps: Vec<PlanStep>,
302    },
303
304    /// Parallel API calls: `const [a, b] = await Promise.all([api.get(...), api.get(...)])`
305    ParallelApiCalls {
306        result_var: String,
307        calls: Vec<(String, String, PathTemplate, Option<ValueExpr>)>, // (temp_var, method, path, body)
308    },
309
310    /// Continue statement: skip to next loop iteration
311    Continue,
312
313    /// Break statement: exit the current loop
314    Break,
315
316    /// MCP tool call: `const result = await mcp.call('server', 'tool', { ... })`
317    #[cfg(feature = "mcp-code-mode")]
318    McpCall {
319        result_var: String,
320        server_id: String,
321        tool_name: String,
322        args: Option<ValueExpr>,
323    },
324
325    /// SDK call: `const result = await api.getCostAndUsage({ start_date: '...' })`
326    SdkCall {
327        result_var: String,
328        operation: String,       // camelCase SDK operation name
329        args: Option<ValueExpr>, // Single object argument
330    },
331}
332
333/// A path template that may contain interpolations.
334#[derive(Debug, Clone, Serialize)]
335pub struct PathTemplate {
336    /// Parts of the path
337    pub parts: Vec<PathPart>,
338}
339
340impl PathTemplate {
341    /// Create a static path.
342    pub fn static_path(path: String) -> Self {
343        Self {
344            parts: vec![PathPart::Literal(path)],
345        }
346    }
347
348    /// Check if this path has any dynamic parts.
349    pub fn is_dynamic(&self) -> bool {
350        self.parts
351            .iter()
352            .any(|p| matches!(p, PathPart::Variable(_) | PathPart::Expression(_)))
353    }
354}
355
356/// A part of a path template.
357#[derive(Debug, Clone, Serialize)]
358pub enum PathPart {
359    /// Literal string
360    Literal(String),
361    /// Variable reference: `${id}`
362    Variable(String),
363    /// Expression: `${user.id}`
364    Expression(ValueExpr),
365}
366
367/// An expression that produces a value.
368#[derive(Debug, Clone, Serialize)]
369pub enum ValueExpr {
370    /// Literal value (null, bool, number, string)
371    Literal(JsonValue),
372
373    /// Variable reference
374    Variable(String),
375
376    /// Property access: `obj.prop`
377    PropertyAccess {
378        object: Box<ValueExpr>,
379        property: String,
380    },
381
382    /// Array index: `arr[0]`
383    ArrayIndex {
384        array: Box<ValueExpr>,
385        index: Box<ValueExpr>,
386    },
387
388    /// Object literal: `{ key: value, ...spread }`
389    ObjectLiteral { fields: Vec<ObjectField> },
390
391    /// Array literal: `[1, 2, 3]`
392    ArrayLiteral { items: Vec<ValueExpr> },
393
394    /// Array method call
395    ArrayMethod {
396        array: Box<ValueExpr>,
397        method: ArrayMethodCall,
398    },
399
400    /// Number method call: `num.toFixed()`, etc.
401    NumberMethod {
402        number: Box<ValueExpr>,
403        method: NumberMethodCall,
404    },
405
406    /// Binary operation: `a + b`, `a === b`, etc.
407    BinaryOp {
408        left: Box<ValueExpr>,
409        op: BinaryOperator,
410        right: Box<ValueExpr>,
411    },
412
413    /// Unary operation: `!a`
414    UnaryOp {
415        op: UnaryOperator,
416        operand: Box<ValueExpr>,
417    },
418
419    /// Ternary: `cond ? a : b`
420    Ternary {
421        condition: Box<ValueExpr>,
422        consequent: Box<ValueExpr>,
423        alternate: Box<ValueExpr>,
424    },
425
426    /// Optional chaining: `obj?.prop`
427    OptionalChain {
428        object: Box<ValueExpr>,
429        property: String,
430    },
431
432    /// Nullish coalescing: `a ?? b`
433    NullishCoalesce {
434        left: Box<ValueExpr>,
435        right: Box<ValueExpr>,
436    },
437
438    /// Await expression (for Promise.all, etc.)
439    Await { expr: Box<ValueExpr> },
440
441    /// Promise.all: `Promise.all([...])`
442    PromiseAll { items: Vec<ValueExpr> },
443
444    /// API call expression (when used inline, not as a statement)
445    ApiCall {
446        method: String,
447        path: PathTemplate,
448        body: Option<Box<ValueExpr>>,
449    },
450
451    /// Block expression with local variable bindings and a final result.
452    /// Used for arrow function block bodies: `x => { const a = x.foo; return a; }`
453    Block {
454        /// Local variable bindings: `[(name, expr), ...]`
455        bindings: Vec<(String, ValueExpr)>,
456        /// The final expression to evaluate and return
457        result: Box<ValueExpr>,
458    },
459
460    /// MCP tool call expression: `mcp.call('server', 'tool', args)`
461    #[cfg(feature = "mcp-code-mode")]
462    McpCall {
463        server_id: String,
464        tool_name: String,
465        args: Option<Box<ValueExpr>>,
466    },
467
468    /// SDK call expression (used in non-assignment contexts): `api.getCostAndUsage({ ... })`
469    SdkCall {
470        operation: String,
471        args: Option<Box<ValueExpr>>,
472    },
473
474    /// Built-in function call: `parseFloat(x)`, `Math.abs(x)`, `Object.keys(obj)`
475    BuiltinCall {
476        func: BuiltinFunction,
477        args: Vec<ValueExpr>,
478    },
479}
480
481/// A field within an object literal, either a regular key-value pair or a spread.
482#[derive(Debug, Clone, Serialize)]
483pub enum ObjectField {
484    /// Regular key-value pair: `key: value`
485    KeyValue { key: String, value: ValueExpr },
486    /// Spread: `...expr`
487    Spread { expr: ValueExpr },
488}
489
490/// Array method calls.
491#[derive(Debug, Clone, Serialize)]
492pub enum ArrayMethodCall {
493    /// `.map(x => expr)`
494    Map {
495        item_var: String,
496        body: Box<ValueExpr>,
497    },
498    /// `.filter(x => expr)`
499    Filter {
500        item_var: String,
501        predicate: Box<ValueExpr>,
502    },
503    /// `.find(x => expr)`
504    Find {
505        item_var: String,
506        predicate: Box<ValueExpr>,
507    },
508    /// `.slice(start, end)`
509    Slice { start: usize, end: Option<usize> },
510    /// `.length`
511    Length,
512    /// `.some(x => expr)`
513    Some {
514        item_var: String,
515        predicate: Box<ValueExpr>,
516    },
517    /// `.every(x => expr)`
518    Every {
519        item_var: String,
520        predicate: Box<ValueExpr>,
521    },
522    /// `.reduce((acc, x) => expr, init)`
523    Reduce {
524        acc_var: String,
525        item_var: String,
526        body: Box<ValueExpr>,
527        initial: Box<ValueExpr>,
528    },
529    /// `.push(item)` - returns new array (pure)
530    Push { item: Box<ValueExpr> },
531    /// `.concat(other)`
532    Concat { other: Box<ValueExpr> },
533    /// `.includes(item)`
534    Includes { item: Box<ValueExpr> },
535    /// `.indexOf(item)`
536    IndexOf { item: Box<ValueExpr> },
537    /// `.join(separator)`
538    Join { separator: Option<String> },
539    /// `.reverse()` - returns new array (pure)
540    Reverse,
541    /// `.sort()` or `.sort((a, b) => expr)` - returns new array (pure)
542    Sort {
543        comparator: Option<(String, String, Box<ValueExpr>)>,
544    },
545    /// `.flat()` - flatten nested arrays
546    Flat,
547    /// `.flatMap(x => expr)`
548    FlatMap {
549        item_var: String,
550        body: Box<ValueExpr>,
551    },
552    /// Get first element: `[0]` or `.at(0)`
553    First,
554    /// Get last element: `.at(-1)`
555    Last,
556    /// `.toLowerCase()` (string-only)
557    ToLowerCase,
558    /// `.toUpperCase()` (string-only)
559    ToUpperCase,
560    /// `.startsWith(searchString)`
561    StartsWith { search: Box<ValueExpr> },
562    /// `.endsWith(searchString)`
563    EndsWith { search: Box<ValueExpr> },
564    /// `.trim()`
565    Trim,
566    /// `.replace(search, replacement)` — first occurrence only
567    Replace {
568        search: Box<ValueExpr>,
569        replacement: Box<ValueExpr>,
570    },
571    /// `.split(separator)`
572    Split { separator: Box<ValueExpr> },
573    /// `.substring(start, end?)`
574    Substring {
575        start: Box<ValueExpr>,
576        end: Option<Box<ValueExpr>>,
577    },
578    /// `.toString()` — works on strings (identity), numbers, arrays, objects
579    ToString,
580}
581
582/// Number method calls.
583#[derive(Debug, Clone, Serialize)]
584pub enum NumberMethodCall {
585    /// `.toFixed(digits)`
586    ToFixed { digits: usize },
587    /// `.toString()`
588    ToString,
589}
590
591/// Built-in global functions and static methods.
592///
593/// These mirror JavaScript built-in functions (parseFloat, parseInt, Number)
594/// and static methods (Math.abs, Object.keys, etc.) that are commonly used
595/// in cost analysis scripts.
596#[derive(Debug, Clone, Serialize)]
597pub enum BuiltinFunction {
598    // Type conversion
599    ParseFloat,
600    ParseInt,
601    NumberCast,
602    // Math static methods
603    MathAbs,
604    MathMax,
605    MathMin,
606    MathRound,
607    MathFloor,
608    MathCeil,
609    // Object static methods
610    ObjectKeys,
611    ObjectValues,
612    ObjectEntries,
613}
614
615/// Binary operators.
616#[derive(Debug, Clone, Copy, Serialize)]
617pub enum BinaryOperator {
618    // Arithmetic
619    Add,
620    Sub,
621    Mul,
622    Div,
623    Mod,
624    // Bitwise (integer truncation)
625    BitwiseOr,
626    // Comparison
627    Eq,
628    NotEq,
629    StrictEq,
630    StrictNotEq,
631    Lt,
632    Lte,
633    Gt,
634    Gte,
635    // Logical
636    And,
637    Or,
638    // String
639    Concat,
640}
641
642/// Unary operators.
643#[derive(Debug, Clone, Copy, Serialize)]
644pub enum UnaryOperator {
645    Not,
646    Neg,
647    Plus,
648    TypeOf,
649}
650
651/// Error during plan compilation.
652#[derive(Debug, thiserror::Error)]
653pub enum CompileError {
654    #[error("Unsupported statement type: {0}")]
655    UnsupportedStatement(String),
656
657    #[error("Unsupported expression type: {0}")]
658    UnsupportedExpression(String),
659
660    #[error("Invalid API call: {0}")]
661    InvalidApiCall(String),
662
663    #[error("Unbounded loop detected")]
664    UnboundedLoop,
665
666    #[error("Invalid path template: {0}")]
667    InvalidPath(String),
668
669    #[error("Too many API calls in plan: {count} (max: {max})")]
670    TooManyApiCalls { count: usize, max: usize },
671
672    #[error("Unsupported array method: {0}")]
673    UnsupportedArrayMethod(String),
674
675    #[error("Parse error: {0}")]
676    ParseError(String),
677
678    #[error("Missing variable name")]
679    MissingVariableName,
680}
681
682/// Result of extracting an API call from an AST expression.
683///
684/// Used by `try_extract_api_call()` to return either an HTTP-mode or SDK-mode call,
685/// enabling downstream code to create the appropriate `PlanStep` or `ValueExpr`.
686enum ExtractedCall {
687    /// HTTP call: `api.get('/path', body)` → `PlanStep::ApiCall` / `ValueExpr::ApiCall`
688    Http {
689        method: String,
690        path: PathTemplate,
691        body: Option<ValueExpr>,
692    },
693    /// SDK call: `api.getCostAndUsage({ ... })` → `PlanStep::SdkCall` / `ValueExpr::SdkCall`
694    Sdk {
695        operation: String,
696        args: Option<ValueExpr>,
697    },
698}
699
700/// Compiler that converts SWC AST to ExecutionPlan.
701pub struct PlanCompiler {
702    api_call_count: usize,
703    endpoints: Vec<String>,
704    methods_used: Vec<String>,
705    has_mutations: bool,
706    /// Set of allowed SDK operation names (camelCase). When non-empty, enables SDK mode.
707    sdk_operations: HashSet<String>,
708    /// Counter for generating unique temp variable names during destructuring.
709    destructure_counter: usize,
710}
711
712impl PlanCompiler {
713    /// Create a new compiler with default settings.
714    pub fn new() -> Self {
715        Self::with_config(&ExecutionConfig::default())
716    }
717
718    /// Create a new compiler with custom config.
719    pub fn with_config(_config: &ExecutionConfig) -> Self {
720        Self {
721            api_call_count: 0,
722            endpoints: Vec::new(),
723            methods_used: Vec::new(),
724            has_mutations: false,
725            sdk_operations: HashSet::new(),
726            destructure_counter: 0,
727        }
728    }
729
730    /// Set the allowed SDK operation names. When non-empty, the compiler operates in SDK mode:
731    /// `api.<operation>(args)` is compiled to `SdkCall` instead of HTTP `ApiCall`.
732    pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
733        self.sdk_operations = operations;
734        self
735    }
736
737    /// Compile JavaScript code to an execution plan.
738    ///
739    /// This is a convenience method that parses the code and compiles it.
740    pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
741        // Parse the JavaScript code using SWC
742        let cm = SourceMap::default();
743        let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
744
745        let lexer = Lexer::new(
746            Syntax::Es(Default::default()),
747            EsVersion::Es2022,
748            StringInput::from(&*fm),
749            None,
750        );
751
752        let mut parser = Parser::new_from(lexer);
753
754        let module = parser.parse_module().map_err(|e| {
755            CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
756        })?;
757
758        // Compile the parsed module
759        self.compile(&module)
760    }
761
762    /// Compile a module to an execution plan.
763    pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
764        let mut steps = Vec::new();
765
766        for item in &module.body {
767            match item {
768                ModuleItem::Stmt(stmt) => {
769                    self.compile_statement(stmt, &mut steps)?;
770                },
771                _ => {
772                    return Err(CompileError::UnsupportedStatement(
773                        "import/export not allowed".into(),
774                    ));
775                },
776            }
777        }
778
779        // Deduplicate methods
780        self.methods_used.sort();
781        self.methods_used.dedup();
782
783        Ok(ExecutionPlan {
784            steps,
785            metadata: PlanMetadata {
786                api_call_count: self.api_call_count,
787                has_mutations: self.has_mutations,
788                endpoints: self.endpoints.clone(),
789                methods_used: self.methods_used.clone(),
790            },
791        })
792    }
793
794    fn compile_statement(
795        &mut self,
796        stmt: &Stmt,
797        steps: &mut Vec<PlanStep>,
798    ) -> Result<(), CompileError> {
799        match stmt {
800            // Variable declaration: const x = ... or const { a, b } = ...
801            Stmt::Decl(Decl::Var(var_decl)) => {
802                for decl in &var_decl.decls {
803                    if let Some(init) = &decl.init {
804                        match &decl.name {
805                            Pat::Ident(ident) => {
806                                let var_name = ident.id.sym.to_string();
807                                self.compile_var_init(&var_name, init, steps)?;
808                            },
809                            Pat::Object(obj_pat) => {
810                                self.compile_object_destructuring(obj_pat, init, steps)?;
811                            },
812                            Pat::Array(arr_pat) => {
813                                self.compile_array_destructuring(arr_pat, init, steps)?;
814                            },
815                            _ => {
816                                return Err(CompileError::UnsupportedExpression(
817                                    "complex destructuring pattern".into(),
818                                ));
819                            },
820                        }
821                    }
822                }
823            },
824
825            // Expression statement: await api.get(...), items.push(x), x = expr, etc.
826            Stmt::Expr(expr_stmt) => {
827                // Handle assignment expressions: `x = expr` or `x = await mcp.call(...)`
828                if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
829                    if assign.op == swc_ecma_ast::AssignOp::Assign {
830                        if let Some(ident) = assign.left.as_ident() {
831                            let var_name = ident.sym.to_string();
832                            self.compile_var_init(&var_name, &assign.right, steps)?;
833                            return Ok(());
834                        }
835                    }
836                }
837
838                let expr = self.compile_expr(&expr_stmt.expr)?;
839                match expr {
840                    // API calls at statement level are tracked as side effects
841                    ValueExpr::ApiCall { method, path, body } => {
842                        self.record_api_call(&method, &path);
843                        steps.push(PlanStep::ApiCall {
844                            result_var: "_".into(), // Discarded result
845                            method,
846                            path,
847                            body: body.map(|b| *b),
848                        });
849                    },
850                    // SDK calls at statement level (discarded result)
851                    ValueExpr::SdkCall { operation, args } => {
852                        steps.push(PlanStep::SdkCall {
853                            result_var: "_".into(), // Discarded result
854                            operation,
855                            args: args.map(|a| *a),
856                        });
857                    },
858                    // Mutating array methods (push, concat) as statements:
859                    // `items.push(x)` → assign the new array back to the variable
860                    ValueExpr::ArrayMethod {
861                        ref array,
862                        ref method,
863                    } if matches!(
864                        method,
865                        ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
866                    ) =>
867                    {
868                        if let ValueExpr::Variable(var_name) = array.as_ref() {
869                            steps.push(PlanStep::Assign {
870                                var: var_name.clone(),
871                                expr,
872                            });
873                        }
874                        // If array is not a simple variable (e.g., obj.arr.push(x)),
875                        // silently discard — same as before.
876                    },
877                    // Other expressions at statement level are discarded
878                    _ => {},
879                }
880            },
881
882            // If statement
883            Stmt::If(if_stmt) => {
884                let condition = self.compile_expr(&if_stmt.test)?;
885                let mut then_steps = Vec::new();
886                self.compile_statement(&if_stmt.cons, &mut then_steps)?;
887
888                let mut else_steps = Vec::new();
889                if let Some(alt) = &if_stmt.alt {
890                    self.compile_statement(alt, &mut else_steps)?;
891                }
892
893                steps.push(PlanStep::Conditional {
894                    condition,
895                    then_steps,
896                    else_steps,
897                });
898            },
899
900            // For-of statement: for (const x of arr) { ... } or for (const { a, b } of arr) { ... }
901            Stmt::ForOf(for_of) => {
902                let (item_var, destructure_bindings) = match &for_of.left {
903                    ForHead::VarDecl(decl) => {
904                        if let Some(first) = decl.decls.first() {
905                            self.extract_loop_var(&first.name)?
906                        } else {
907                            return Err(CompileError::MissingVariableName);
908                        }
909                    },
910                    ForHead::Pat(pat) => self.extract_loop_var(pat)?,
911                    _ => return Err(CompileError::MissingVariableName),
912                };
913
914                let collection = self.compile_expr(&for_of.right)?;
915
916                // Check if collection is bounded (has .slice())
917                let max_iterations = self.extract_bound(&collection).unwrap_or(100);
918
919                let mut body = destructure_bindings;
920                self.compile_statement(&for_of.body, &mut body)?;
921
922                steps.push(PlanStep::BoundedLoop {
923                    item_var,
924                    collection,
925                    max_iterations,
926                    body,
927                });
928            },
929
930            // Block statement: { ... }
931            Stmt::Block(block) => {
932                for stmt in &block.stmts {
933                    self.compile_statement(stmt, steps)?;
934                }
935            },
936
937            // Return statement
938            Stmt::Return(ret) => {
939                let value = if let Some(arg) = &ret.arg {
940                    self.compile_expr(arg)?
941                } else {
942                    ValueExpr::Literal(JsonValue::Null)
943                };
944                steps.push(PlanStep::Return { value });
945            },
946
947            // Empty statement
948            Stmt::Empty(_) => {},
949
950            // Try/catch statement
951            Stmt::Try(try_stmt) => {
952                let mut try_steps = Vec::new();
953                for stmt in &try_stmt.block.stmts {
954                    self.compile_statement(stmt, &mut try_steps)?;
955                }
956
957                let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
958                    let var_name = handler.param.as_ref().map(|p| match p {
959                        swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
960                        _ => "error".to_string(),
961                    });
962                    let mut catch_stmts = Vec::new();
963                    for stmt in &handler.body.stmts {
964                        self.compile_statement(stmt, &mut catch_stmts)?;
965                    }
966                    (var_name, catch_stmts)
967                } else {
968                    (None, Vec::new())
969                };
970
971                let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
972                    let mut finally_stmts = Vec::new();
973                    for stmt in &finalizer.stmts {
974                        self.compile_statement(stmt, &mut finally_stmts)?;
975                    }
976                    finally_stmts
977                } else {
978                    Vec::new()
979                };
980
981                steps.push(PlanStep::TryCatch {
982                    try_steps,
983                    catch_var,
984                    catch_steps,
985                    finally_steps,
986                });
987            },
988
989            // Continue statement: skip to next loop iteration
990            Stmt::Continue(_) => {
991                steps.push(PlanStep::Continue);
992            },
993
994            // Break statement: exit the current loop
995            Stmt::Break(_) => {
996                steps.push(PlanStep::Break);
997            },
998
999            Stmt::Decl(decl) => {
1000                let msg = match decl {
1001                    Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
1002                    Decl::Class(_) => "Class declarations are not supported",
1003                    _ => "This declaration type is not supported",
1004                };
1005                return Err(CompileError::UnsupportedStatement(msg.into()));
1006            },
1007            Stmt::Switch(_) => {
1008                return Err(CompileError::UnsupportedStatement(
1009                    "'switch' statements are not supported. Use if/else if/else instead".into(),
1010                ));
1011            },
1012            Stmt::Throw(_) => {
1013                return Err(CompileError::UnsupportedStatement(
1014                    "'throw' statements are not supported. Use try/catch for error handling".into(),
1015                ));
1016            },
1017            Stmt::While(_) => {
1018                return Err(CompileError::UnsupportedStatement(
1019                    "'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1020                ));
1021            },
1022            Stmt::DoWhile(_) => {
1023                return Err(CompileError::UnsupportedStatement(
1024                    "'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1025                ));
1026            },
1027            Stmt::For(_) => {
1028                return Err(CompileError::UnsupportedStatement(
1029                    "'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1030                ));
1031            },
1032            Stmt::ForIn(_) => {
1033                return Err(CompileError::UnsupportedStatement(
1034                    "'for-in' loops are not supported. Use for-of with .slice() instead".into(),
1035                ));
1036            },
1037            Stmt::Labeled(_) => {
1038                return Err(CompileError::UnsupportedStatement(
1039                    "Labeled statements are not supported".into(),
1040                ));
1041            },
1042            Stmt::With(_) => {
1043                return Err(CompileError::UnsupportedStatement(
1044                    "'with' statements are not supported".into(),
1045                ));
1046            },
1047            Stmt::Debugger(_) => {
1048                return Err(CompileError::UnsupportedStatement(
1049                    "'debugger' statements are not supported".into(),
1050                ));
1051            },
1052        }
1053
1054        Ok(())
1055    }
1056
1057    fn compile_var_init(
1058        &mut self,
1059        var_name: &str,
1060        init: &Expr,
1061        steps: &mut Vec<PlanStep>,
1062    ) -> Result<(), CompileError> {
1063        // Check if this is an await expression
1064        if let Expr::Await(await_expr) = init {
1065            // Check if awaiting an API call or SDK call
1066            if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1067                match extracted {
1068                    ExtractedCall::Http { method, path, body } => {
1069                        self.record_api_call(&method, &path);
1070                        steps.push(PlanStep::ApiCall {
1071                            result_var: var_name.into(),
1072                            method,
1073                            path,
1074                            body,
1075                        });
1076                    },
1077                    ExtractedCall::Sdk { operation, args } => {
1078                        steps.push(PlanStep::SdkCall {
1079                            result_var: var_name.into(),
1080                            operation,
1081                            args,
1082                        });
1083                    },
1084                }
1085                return Ok(());
1086            }
1087
1088            // Check if awaiting an MCP call: const x = await mcp.call(...)
1089            #[cfg(feature = "mcp-code-mode")]
1090            if let Some((server_id, tool_name, args)) =
1091                self.try_extract_mcp_call(&await_expr.arg)?
1092            {
1093                steps.push(PlanStep::McpCall {
1094                    result_var: var_name.into(),
1095                    server_id,
1096                    tool_name,
1097                    args,
1098                });
1099                return Ok(());
1100            }
1101
1102            // Check if awaiting Promise.all([api calls...])
1103            if let Expr::Call(call) = await_expr.arg.as_ref() {
1104                let inner = self.compile_call(call)?;
1105                if let ValueExpr::PromiseAll { items } = inner {
1106                    return self.compile_promise_all(var_name, items, steps);
1107                }
1108            }
1109        }
1110
1111        // Regular assignment
1112        let expr = self.compile_expr(init)?;
1113        steps.push(PlanStep::Assign {
1114            var: var_name.into(),
1115            expr,
1116        });
1117        Ok(())
1118    }
1119
1120    /// Compile `await Promise.all([api.get(...), api.get(...)])` into a ParallelApiCalls step.
1121    /// Falls back to sequential execution if any item is not an API call.
1122    fn compile_promise_all(
1123        &mut self,
1124        result_var: &str,
1125        items: Vec<ValueExpr>,
1126        steps: &mut Vec<PlanStep>,
1127    ) -> Result<(), CompileError> {
1128        let mut calls = Vec::new();
1129        let mut all_api_calls = true;
1130
1131        for (i, item) in items.iter().enumerate() {
1132            match item {
1133                ValueExpr::ApiCall { method, path, body } => {
1134                    let temp_var = format!("__promise_all_{}_{}", result_var, i);
1135                    calls.push((
1136                        temp_var,
1137                        method.clone(),
1138                        path.clone(),
1139                        body.as_ref().map(|b| *b.clone()),
1140                    ));
1141                },
1142                _ => {
1143                    all_api_calls = false;
1144                    break;
1145                },
1146            }
1147        }
1148
1149        if all_api_calls && !calls.is_empty() {
1150            // Record all API calls for metadata
1151            for (_, method, path, _) in &calls {
1152                self.record_api_call(method, path);
1153            }
1154            steps.push(PlanStep::ParallelApiCalls {
1155                result_var: result_var.into(),
1156                calls,
1157            });
1158            Ok(())
1159        } else {
1160            // Fallback: execute items sequentially as individual API calls and collect results
1161            // This handles mixed expressions in Promise.all
1162            Err(CompileError::UnsupportedExpression(
1163                "Promise.all with non-API-call expressions".into(),
1164            ))
1165        }
1166    }
1167
1168    fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
1169        match expr {
1170            // Literal values
1171            Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
1172
1173            // Variable reference
1174            Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
1175
1176            // Property access: obj.prop
1177            Expr::Member(member) => {
1178                let object = Box::new(self.compile_expr(&member.obj)?);
1179
1180                // Check if this is an array method call
1181                if let MemberProp::Ident(prop) = &member.prop {
1182                    let prop_name = prop.sym.to_string();
1183                    if prop_name == "length" {
1184                        return Ok(ValueExpr::ArrayMethod {
1185                            array: object,
1186                            method: ArrayMethodCall::Length,
1187                        });
1188                    }
1189                }
1190
1191                match &member.prop {
1192                    MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
1193                        object,
1194                        property: ident.sym.to_string(),
1195                    }),
1196                    MemberProp::Computed(computed) => {
1197                        let index = Box::new(self.compile_expr(&computed.expr)?);
1198                        Ok(ValueExpr::ArrayIndex {
1199                            array: object,
1200                            index,
1201                        })
1202                    }
1203                    _ => Err(CompileError::UnsupportedExpression("private property".into())),
1204                }
1205            }
1206
1207            // Call expression: fn(), api.get(), arr.map(), etc.
1208            Expr::Call(call) => self.compile_call(call),
1209
1210            // Object literal: { key: value, ...spread }
1211            Expr::Object(obj) => {
1212                let mut fields = Vec::new();
1213                for prop in &obj.props {
1214                    match prop {
1215                        PropOrSpread::Prop(prop) => {
1216                            if let Prop::KeyValue(kv) = prop.as_ref() {
1217                                let key = self.prop_name_to_string(&kv.key)?;
1218                                let value = self.compile_expr(&kv.value)?;
1219                                fields.push(ObjectField::KeyValue { key, value });
1220                            } else if let Prop::Shorthand(ident) = prop.as_ref() {
1221                                let name = ident.sym.to_string();
1222                                fields.push(ObjectField::KeyValue {
1223                                    key: name.clone(),
1224                                    value: ValueExpr::Variable(name),
1225                                });
1226                            }
1227                        }
1228                        PropOrSpread::Spread(spread) => {
1229                            let expr = self.compile_expr(&spread.expr)?;
1230                            fields.push(ObjectField::Spread { expr });
1231                        }
1232                    }
1233                }
1234                Ok(ValueExpr::ObjectLiteral { fields })
1235            }
1236
1237            // Array literal: [1, 2, 3]
1238            Expr::Array(arr) => {
1239                let mut items = Vec::new();
1240                for elem in arr.elems.iter().flatten() {
1241                    if elem.spread.is_some() {
1242                        return Err(CompileError::UnsupportedExpression("spread".into()));
1243                    }
1244                    items.push(self.compile_expr(&elem.expr)?);
1245                }
1246                Ok(ValueExpr::ArrayLiteral { items })
1247            }
1248
1249            // Template literal: `Hello ${name}`
1250            Expr::Tpl(tpl) => {
1251                // For now, treat as string concatenation
1252                // This is used primarily for path templates
1253                let mut parts = Vec::new();
1254                for (i, quasi) in tpl.quasis.iter().enumerate() {
1255                    let raw = quasi.raw.to_string();
1256                    if !raw.is_empty() {
1257                        parts.push(ValueExpr::Literal(JsonValue::String(raw)));
1258                    }
1259                    if i < tpl.exprs.len() {
1260                        parts.push(self.compile_expr(&tpl.exprs[i])?);
1261                    }
1262                }
1263
1264                // If single part, return it directly
1265                if parts.len() == 1 {
1266                    return Ok(parts.remove(0));
1267                }
1268
1269                // Otherwise, build concatenation
1270                let mut result = parts.remove(0);
1271                for part in parts {
1272                    result = ValueExpr::BinaryOp {
1273                        left: Box::new(result),
1274                        op: BinaryOperator::Concat,
1275                        right: Box::new(part),
1276                    };
1277                }
1278                Ok(result)
1279            }
1280
1281            // Binary expression: a + b, a === b
1282            Expr::Bin(bin) => {
1283                let left = Box::new(self.compile_expr(&bin.left)?);
1284                let right = Box::new(self.compile_expr(&bin.right)?);
1285                let op = self.compile_bin_op(bin.op)?;
1286                Ok(ValueExpr::BinaryOp { left, op, right })
1287            }
1288
1289            // Unary expression: !a, -a
1290            Expr::Unary(unary) => {
1291                let operand = Box::new(self.compile_expr(&unary.arg)?);
1292                let op = match unary.op {
1293                    UnaryOp::Bang => UnaryOperator::Not,
1294                    UnaryOp::Minus => UnaryOperator::Neg,
1295                    UnaryOp::TypeOf => UnaryOperator::TypeOf,
1296                    UnaryOp::Plus => UnaryOperator::Plus,
1297                    _ => return Err(CompileError::UnsupportedExpression("unary op".into())),
1298                };
1299                Ok(ValueExpr::UnaryOp { op, operand })
1300            }
1301
1302            // Conditional/ternary: cond ? a : b
1303            Expr::Cond(cond) => {
1304                let condition = Box::new(self.compile_expr(&cond.test)?);
1305                let consequent = Box::new(self.compile_expr(&cond.cons)?);
1306                let alternate = Box::new(self.compile_expr(&cond.alt)?);
1307                Ok(ValueExpr::Ternary {
1308                    condition,
1309                    consequent,
1310                    alternate,
1311                })
1312            }
1313
1314            // Await expression
1315            Expr::Await(await_expr) => {
1316                // Check if it's an API call or SDK call
1317                if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1318                    return match extracted {
1319                        ExtractedCall::Http { method, path, body } => {
1320                            self.record_api_call(&method, &path);
1321                            Ok(ValueExpr::ApiCall {
1322                                method,
1323                                path,
1324                                body: body.map(Box::new),
1325                            })
1326                        }
1327                        ExtractedCall::Sdk { operation, args } => {
1328                            Ok(ValueExpr::SdkCall {
1329                                operation,
1330                                args: args.map(Box::new),
1331                            })
1332                        }
1333                    };
1334                }
1335                // Check if it's an MCP call
1336                #[cfg(feature = "mcp-code-mode")]
1337                if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
1338                    return Ok(ValueExpr::McpCall {
1339                        server_id,
1340                        tool_name,
1341                        args: args.map(Box::new),
1342                    });
1343                }
1344                // Otherwise, just await the inner expression
1345                let inner = self.compile_expr(&await_expr.arg)?;
1346                Ok(ValueExpr::Await {
1347                    expr: Box::new(inner),
1348                })
1349            }
1350
1351            // Arrow function (for use in .map(), .filter(), etc.)
1352            Expr::Arrow(_) => {
1353                // Arrow functions are handled specially in array method compilation
1354                Err(CompileError::UnsupportedExpression(
1355                    "arrow function outside array method".into(),
1356                ))
1357            }
1358
1359            // Parenthesized expression
1360            Expr::Paren(paren) => self.compile_expr(&paren.expr),
1361
1362            // Optional chaining: obj?.prop
1363            Expr::OptChain(opt) => {
1364                match opt.base.as_ref() {
1365                    OptChainBase::Member(member) => {
1366                        let object = Box::new(self.compile_expr(&member.obj)?);
1367                        if let MemberProp::Ident(ident) = &member.prop {
1368                            Ok(ValueExpr::OptionalChain {
1369                                object,
1370                                property: ident.sym.to_string(),
1371                            })
1372                        } else {
1373                            Err(CompileError::UnsupportedExpression("computed optional chain".into()))
1374                        }
1375                    }
1376                    _ => Err(CompileError::UnsupportedExpression("optional call".into())),
1377                }
1378            }
1379
1380            Expr::This(_) => Err(CompileError::UnsupportedExpression(
1381                "'this' keyword is not supported".into(),
1382            )),
1383            Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
1384                "Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
1385            )),
1386            Expr::Update(_) => Err(CompileError::UnsupportedExpression(
1387                "Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
1388            )),
1389            Expr::New(_) => Err(CompileError::UnsupportedExpression(
1390                "'new' keyword is not supported".into(),
1391            )),
1392            Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
1393                "Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
1394            )),
1395            Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
1396                "Tagged template literals are not supported. Use regular template literals instead".into(),
1397            )),
1398            Expr::Class(_) => Err(CompileError::UnsupportedExpression(
1399                "Class expressions are not supported".into(),
1400            )),
1401            Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
1402                "Generator yield is not supported".into(),
1403            )),
1404            Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
1405                "'super' is not supported".into(),
1406            )),
1407            Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
1408                "Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
1409            )),
1410            _ => Err(CompileError::UnsupportedExpression(
1411                "This expression type is not supported in the JavaScript subset".into(),
1412            )),
1413        }
1414    }
1415
1416    fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
1417        // Check if this is an API call (HTTP) or SDK call
1418        if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
1419            return match extracted {
1420                ExtractedCall::Http { method, path, body } => {
1421                    self.record_api_call(&method, &path);
1422                    Ok(ValueExpr::ApiCall {
1423                        method,
1424                        path,
1425                        body: body.map(Box::new),
1426                    })
1427                },
1428                ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
1429                    operation,
1430                    args: args.map(Box::new),
1431                }),
1432            };
1433        }
1434
1435        // Check if this is an MCP call: mcp.call('server', 'tool', args)
1436        #[cfg(feature = "mcp-code-mode")]
1437        if let Some((server_id, tool_name, args)) =
1438            self.try_extract_mcp_call(&Expr::Call(call.clone()))?
1439        {
1440            return Ok(ValueExpr::McpCall {
1441                server_id,
1442                tool_name,
1443                args: args.map(Box::new),
1444            });
1445        }
1446
1447        // Check if this is Promise.all
1448        if let Callee::Expr(callee) = &call.callee {
1449            if let Expr::Member(member) = callee.as_ref() {
1450                if let Expr::Ident(obj) = member.obj.as_ref() {
1451                    if obj.sym.as_ref() == "Promise" {
1452                        if let MemberProp::Ident(prop) = &member.prop {
1453                            if prop.sym.as_ref() == "all" {
1454                                if let Some(arg) = call.args.first() {
1455                                    if let Expr::Array(arr) = arg.expr.as_ref() {
1456                                        let mut items = Vec::new();
1457                                        for elem in arr.elems.iter().flatten() {
1458                                            items.push(self.compile_expr(&elem.expr)?);
1459                                        }
1460                                        return Ok(ValueExpr::PromiseAll { items });
1461                                    }
1462                                }
1463                            }
1464                        }
1465                    }
1466                }
1467            }
1468        }
1469
1470        // Check if this is an array method: arr.map(), arr.filter(), etc.
1471        if let Callee::Expr(callee) = &call.callee {
1472            if let Expr::Member(member) = callee.as_ref() {
1473                let array = Box::new(self.compile_expr(&member.obj)?);
1474
1475                if let MemberProp::Ident(method_ident) = &member.prop {
1476                    let method_name = method_ident.sym.as_ref();
1477
1478                    match method_name {
1479                        "map" => {
1480                            let (item_var, body) = self.extract_arrow_callback(call)?;
1481                            return Ok(ValueExpr::ArrayMethod {
1482                                array,
1483                                method: ArrayMethodCall::Map {
1484                                    item_var,
1485                                    body: Box::new(body),
1486                                },
1487                            });
1488                        },
1489                        "filter" => {
1490                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1491                            return Ok(ValueExpr::ArrayMethod {
1492                                array,
1493                                method: ArrayMethodCall::Filter {
1494                                    item_var,
1495                                    predicate: Box::new(predicate),
1496                                },
1497                            });
1498                        },
1499                        "find" => {
1500                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1501                            return Ok(ValueExpr::ArrayMethod {
1502                                array,
1503                                method: ArrayMethodCall::Find {
1504                                    item_var,
1505                                    predicate: Box::new(predicate),
1506                                },
1507                            });
1508                        },
1509                        "some" => {
1510                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1511                            return Ok(ValueExpr::ArrayMethod {
1512                                array,
1513                                method: ArrayMethodCall::Some {
1514                                    item_var,
1515                                    predicate: Box::new(predicate),
1516                                },
1517                            });
1518                        },
1519                        "every" => {
1520                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1521                            return Ok(ValueExpr::ArrayMethod {
1522                                array,
1523                                method: ArrayMethodCall::Every {
1524                                    item_var,
1525                                    predicate: Box::new(predicate),
1526                                },
1527                            });
1528                        },
1529                        "flatMap" => {
1530                            let (item_var, body) = self.extract_arrow_callback(call)?;
1531                            return Ok(ValueExpr::ArrayMethod {
1532                                array,
1533                                method: ArrayMethodCall::FlatMap {
1534                                    item_var,
1535                                    body: Box::new(body),
1536                                },
1537                            });
1538                        },
1539                        "slice" => {
1540                            let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1541                            let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
1542                            return Ok(ValueExpr::ArrayMethod {
1543                                array,
1544                                method: ArrayMethodCall::Slice { start, end },
1545                            });
1546                        },
1547                        "push" => {
1548                            if let Some(arg) = call.args.first() {
1549                                let item = Box::new(self.compile_expr(&arg.expr)?);
1550                                return Ok(ValueExpr::ArrayMethod {
1551                                    array,
1552                                    method: ArrayMethodCall::Push { item },
1553                                });
1554                            }
1555                        },
1556                        "concat" => {
1557                            if let Some(arg) = call.args.first() {
1558                                let other = Box::new(self.compile_expr(&arg.expr)?);
1559                                return Ok(ValueExpr::ArrayMethod {
1560                                    array,
1561                                    method: ArrayMethodCall::Concat { other },
1562                                });
1563                            }
1564                        },
1565                        "includes" => {
1566                            if let Some(arg) = call.args.first() {
1567                                let item = Box::new(self.compile_expr(&arg.expr)?);
1568                                return Ok(ValueExpr::ArrayMethod {
1569                                    array,
1570                                    method: ArrayMethodCall::Includes { item },
1571                                });
1572                            }
1573                        },
1574                        "indexOf" => {
1575                            if let Some(arg) = call.args.first() {
1576                                let item = Box::new(self.compile_expr(&arg.expr)?);
1577                                return Ok(ValueExpr::ArrayMethod {
1578                                    array,
1579                                    method: ArrayMethodCall::IndexOf { item },
1580                                });
1581                            }
1582                        },
1583                        "join" => {
1584                            let separator = if let Some(arg) = call.args.first() {
1585                                if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
1586                                    Some(s.value.to_string_lossy().into_owned())
1587                                } else {
1588                                    None
1589                                }
1590                            } else {
1591                                None
1592                            };
1593                            return Ok(ValueExpr::ArrayMethod {
1594                                array,
1595                                method: ArrayMethodCall::Join { separator },
1596                            });
1597                        },
1598                        "reverse" => {
1599                            return Ok(ValueExpr::ArrayMethod {
1600                                array,
1601                                method: ArrayMethodCall::Reverse,
1602                            });
1603                        },
1604                        "sort" => {
1605                            let comparator = if !call.args.is_empty() {
1606                                let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
1607                                Some((a_var, b_var, Box::new(body)))
1608                            } else {
1609                                None
1610                            };
1611                            return Ok(ValueExpr::ArrayMethod {
1612                                array,
1613                                method: ArrayMethodCall::Sort { comparator },
1614                            });
1615                        },
1616                        "flat" => {
1617                            return Ok(ValueExpr::ArrayMethod {
1618                                array,
1619                                method: ArrayMethodCall::Flat,
1620                            });
1621                        },
1622                        "at" => {
1623                            if let Some(n) = self.extract_number_arg(call, 0)? {
1624                                if n == 0 {
1625                                    return Ok(ValueExpr::ArrayMethod {
1626                                        array,
1627                                        method: ArrayMethodCall::First,
1628                                    });
1629                                } else if n == -1 {
1630                                    return Ok(ValueExpr::ArrayMethod {
1631                                        array,
1632                                        method: ArrayMethodCall::Last,
1633                                    });
1634                                }
1635                            }
1636                        },
1637                        // reduce((acc, item) => expr, initialValue)
1638                        "reduce" if call.args.len() >= 2 => {
1639                            let (acc_var, item_var, body) = self.extract_reduce_callback(call)?;
1640                            let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
1641                            return Ok(ValueExpr::ArrayMethod {
1642                                array,
1643                                method: ArrayMethodCall::Reduce {
1644                                    acc_var,
1645                                    item_var,
1646                                    body: Box::new(body),
1647                                    initial,
1648                                },
1649                            });
1650                        },
1651                        "toFixed" => {
1652                            // Number.toFixed(digits) - treat as a number method
1653                            let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1654                            return Ok(ValueExpr::NumberMethod {
1655                                number: array, // The "array" here is actually the number
1656                                method: NumberMethodCall::ToFixed { digits },
1657                            });
1658                        },
1659                        "toLowerCase" => {
1660                            return Ok(ValueExpr::ArrayMethod {
1661                                array,
1662                                method: ArrayMethodCall::ToLowerCase,
1663                            });
1664                        },
1665                        "toUpperCase" => {
1666                            return Ok(ValueExpr::ArrayMethod {
1667                                array,
1668                                method: ArrayMethodCall::ToUpperCase,
1669                            });
1670                        },
1671                        "startsWith" => {
1672                            let arg = call.args.first().ok_or_else(|| {
1673                                CompileError::UnsupportedExpression(
1674                                    "startsWith() requires a search argument".into(),
1675                                )
1676                            })?;
1677                            let search = Box::new(self.compile_expr(&arg.expr)?);
1678                            return Ok(ValueExpr::ArrayMethod {
1679                                array,
1680                                method: ArrayMethodCall::StartsWith { search },
1681                            });
1682                        },
1683                        "endsWith" => {
1684                            let arg = call.args.first().ok_or_else(|| {
1685                                CompileError::UnsupportedExpression(
1686                                    "endsWith() requires a search argument".into(),
1687                                )
1688                            })?;
1689                            let search = Box::new(self.compile_expr(&arg.expr)?);
1690                            return Ok(ValueExpr::ArrayMethod {
1691                                array,
1692                                method: ArrayMethodCall::EndsWith { search },
1693                            });
1694                        },
1695                        "trim" => {
1696                            return Ok(ValueExpr::ArrayMethod {
1697                                array,
1698                                method: ArrayMethodCall::Trim,
1699                            });
1700                        },
1701                        "replace" => {
1702                            if call.args.len() < 2 {
1703                                return Err(CompileError::UnsupportedExpression(
1704                                    "replace() requires search and replacement arguments".into(),
1705                                ));
1706                            }
1707                            let search = Box::new(self.compile_expr(&call.args[0].expr)?);
1708                            let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
1709                            return Ok(ValueExpr::ArrayMethod {
1710                                array,
1711                                method: ArrayMethodCall::Replace {
1712                                    search,
1713                                    replacement,
1714                                },
1715                            });
1716                        },
1717                        "split" => {
1718                            let arg = call.args.first().ok_or_else(|| {
1719                                CompileError::UnsupportedExpression(
1720                                    "split() requires a separator argument".into(),
1721                                )
1722                            })?;
1723                            let separator = Box::new(self.compile_expr(&arg.expr)?);
1724                            return Ok(ValueExpr::ArrayMethod {
1725                                array,
1726                                method: ArrayMethodCall::Split { separator },
1727                            });
1728                        },
1729                        "substring" => {
1730                            let arg = call.args.first().ok_or_else(|| {
1731                                CompileError::UnsupportedExpression(
1732                                    "substring() requires a start argument".into(),
1733                                )
1734                            })?;
1735                            let start = Box::new(self.compile_expr(&arg.expr)?);
1736                            let end = if call.args.len() >= 2 {
1737                                Some(Box::new(self.compile_expr(&call.args[1].expr)?))
1738                            } else {
1739                                None
1740                            };
1741                            return Ok(ValueExpr::ArrayMethod {
1742                                array,
1743                                method: ArrayMethodCall::Substring { start, end },
1744                            });
1745                        },
1746                        "toString" => {
1747                            return Ok(ValueExpr::ArrayMethod {
1748                                array,
1749                                method: ArrayMethodCall::ToString,
1750                            });
1751                        },
1752                        _ => {},
1753                    }
1754                }
1755            }
1756        }
1757
1758        // Check for built-in global functions: parseFloat(), parseInt(), Number()
1759        if let Callee::Expr(callee) = &call.callee {
1760            if let Expr::Ident(ident) = callee.as_ref() {
1761                let func = match ident.sym.as_ref() {
1762                    "parseFloat" => Some(BuiltinFunction::ParseFloat),
1763                    "parseInt" => Some(BuiltinFunction::ParseInt),
1764                    "Number" => Some(BuiltinFunction::NumberCast),
1765                    _ => None,
1766                };
1767                if let Some(func) = func {
1768                    let args = call
1769                        .args
1770                        .iter()
1771                        .map(|a| self.compile_expr(&a.expr))
1772                        .collect::<Result<Vec<_>, _>>()?;
1773                    return Ok(ValueExpr::BuiltinCall { func, args });
1774                }
1775            }
1776
1777            // Check for static method calls: Math.abs(), Object.keys(), etc.
1778            if let Expr::Member(member) = callee.as_ref() {
1779                if let Expr::Ident(obj) = member.obj.as_ref() {
1780                    if let MemberProp::Ident(prop) = &member.prop {
1781                        let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
1782                            ("Math", "abs") => Some(BuiltinFunction::MathAbs),
1783                            ("Math", "max") => Some(BuiltinFunction::MathMax),
1784                            ("Math", "min") => Some(BuiltinFunction::MathMin),
1785                            ("Math", "round") => Some(BuiltinFunction::MathRound),
1786                            ("Math", "floor") => Some(BuiltinFunction::MathFloor),
1787                            ("Math", "ceil") => Some(BuiltinFunction::MathCeil),
1788                            ("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
1789                            ("Object", "values") => Some(BuiltinFunction::ObjectValues),
1790                            ("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
1791                            _ => None,
1792                        };
1793                        if let Some(func) = func {
1794                            let args = call
1795                                .args
1796                                .iter()
1797                                .map(|a| self.compile_expr(&a.expr))
1798                                .collect::<Result<Vec<_>, _>>()?;
1799                            return Ok(ValueExpr::BuiltinCall { func, args });
1800                        }
1801                    }
1802                }
1803            }
1804        }
1805
1806        Err(CompileError::UnsupportedExpression("function call".into()))
1807    }
1808
1809    fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
1810        let call = match expr {
1811            Expr::Call(c) => c,
1812            _ => return Ok(None),
1813        };
1814
1815        if let Callee::Expr(callee) = &call.callee {
1816            if let Expr::Member(member) = callee.as_ref() {
1817                if let Expr::Ident(obj) = member.obj.as_ref() {
1818                    if obj.sym.as_ref() == "api" {
1819                        if let MemberProp::Ident(method_ident) = &member.prop {
1820                            let method_name = method_ident.sym.as_ref();
1821
1822                            if !self.sdk_operations.is_empty() {
1823                                // SDK mode: validate against allowed operation names
1824                                if !self.sdk_operations.contains(method_name) {
1825                                    return Err(CompileError::InvalidApiCall(format!(
1826                                        "Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
1827                                        method_name
1828                                    )));
1829                                }
1830                                let args = if let Some(arg) = call.args.first() {
1831                                    Some(self.compile_expr(&arg.expr)?)
1832                                } else {
1833                                    None
1834                                };
1835                                self.api_call_count += 1;
1836                                let op_endpoint = format!("sdk:{}", method_name);
1837                                if !self.endpoints.contains(&op_endpoint) {
1838                                    self.endpoints.push(op_endpoint);
1839                                }
1840                                if !self.methods_used.contains(&method_name.to_string()) {
1841                                    self.methods_used.push(method_name.to_string());
1842                                }
1843                                return Ok(Some(ExtractedCall::Sdk {
1844                                    operation: method_name.to_string(),
1845                                    args,
1846                                }));
1847                            }
1848
1849                            // HTTP mode: validate it's a known HTTP method
1850                            if HttpMethod::from_str(method_name).is_none() {
1851                                return Err(CompileError::InvalidApiCall(format!(
1852                                    "Unknown method: api.{}",
1853                                    method_name
1854                                )));
1855                            }
1856
1857                            // Extract path from first argument
1858                            let path = if let Some(arg) = call.args.first() {
1859                                self.extract_path_template(&arg.expr)?
1860                            } else {
1861                                return Err(CompileError::InvalidApiCall(
1862                                    "API call requires path".into(),
1863                                ));
1864                            };
1865
1866                            // Extract body from second argument (for POST, PUT, PATCH)
1867                            let body = if let Some(arg) = call.args.get(1) {
1868                                Some(self.compile_expr(&arg.expr)?)
1869                            } else {
1870                                None
1871                            };
1872
1873                            return Ok(Some(ExtractedCall::Http {
1874                                method: method_name.to_uppercase(),
1875                                path,
1876                                body,
1877                            }));
1878                        }
1879                    }
1880                }
1881            }
1882        }
1883
1884        Ok(None)
1885    }
1886
1887    /// Try to extract an MCP call: `mcp.call('server', 'tool', { args })`
1888    ///
1889    /// Returns `(server_id, tool_name, args)` if the expression is an MCP call.
1890    #[cfg(feature = "mcp-code-mode")]
1891    fn try_extract_mcp_call(
1892        &mut self,
1893        expr: &Expr,
1894    ) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
1895        let call = match expr {
1896            Expr::Call(c) => c,
1897            _ => return Ok(None),
1898        };
1899
1900        if let Callee::Expr(callee) = &call.callee {
1901            if let Expr::Member(member) = callee.as_ref() {
1902                if let Expr::Ident(obj) = member.obj.as_ref() {
1903                    if obj.sym.as_ref() == "mcp" {
1904                        if let MemberProp::Ident(method_ident) = &member.prop {
1905                            if method_ident.sym.as_ref() == "call" {
1906                                // Extract server_id from first arg (string literal)
1907                                let server_id = call.args.first()
1908                                    .and_then(|a| {
1909                                        if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1910                                            Some(s.value.to_string_lossy().into_owned())
1911                                        } else {
1912                                            None
1913                                        }
1914                                    })
1915                                    .ok_or_else(|| CompileError::UnsupportedExpression(
1916                                        "mcp.call() first argument must be a string literal (server_id)".into(),
1917                                    ))?;
1918
1919                                // Extract tool_name from second arg (string literal)
1920                                let tool_name = call.args.get(1)
1921                                    .and_then(|a| {
1922                                        if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1923                                            Some(s.value.to_string_lossy().into_owned())
1924                                        } else {
1925                                            None
1926                                        }
1927                                    })
1928                                    .ok_or_else(|| CompileError::UnsupportedExpression(
1929                                        "mcp.call() second argument must be a string literal (tool_name)".into(),
1930                                    ))?;
1931
1932                                // Extract args from third arg (optional object expression)
1933                                let args = call
1934                                    .args
1935                                    .get(2)
1936                                    .map(|a| self.compile_expr(&a.expr))
1937                                    .transpose()?;
1938
1939                                return Ok(Some((server_id, tool_name, args)));
1940                            }
1941                        }
1942                    }
1943                }
1944            }
1945        }
1946
1947        Ok(None)
1948    }
1949
1950    fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
1951        match expr {
1952            // Simple string: '/users'
1953            Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
1954                s.value.to_string_lossy().into_owned(),
1955            )),
1956
1957            // Template literal: `/users/${id}`
1958            Expr::Tpl(tpl) => {
1959                let mut parts = Vec::new();
1960                for (i, quasi) in tpl.quasis.iter().enumerate() {
1961                    let raw = quasi.raw.to_string();
1962                    if !raw.is_empty() {
1963                        parts.push(PathPart::Literal(raw));
1964                    }
1965                    if i < tpl.exprs.len() {
1966                        // Check if it's a simple variable
1967                        if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
1968                            parts.push(PathPart::Variable(ident.sym.to_string()));
1969                        } else {
1970                            // Complex expression
1971                            let expr = self.compile_expr(&tpl.exprs[i])?;
1972                            parts.push(PathPart::Expression(expr));
1973                        }
1974                    }
1975                }
1976                Ok(PathTemplate { parts })
1977            },
1978
1979            _ => Err(CompileError::InvalidPath(
1980                "Path must be a string or template literal".into(),
1981            )),
1982        }
1983    }
1984
1985    fn extract_arrow_callback(
1986        &mut self,
1987        call: &CallExpr,
1988    ) -> Result<(String, ValueExpr), CompileError> {
1989        let arg = call
1990            .args
1991            .first()
1992            .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
1993
1994        if let Expr::Arrow(arrow) = arg.expr.as_ref() {
1995            // Get parameter name
1996            let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
1997                ident.id.sym.to_string()
1998            } else {
1999                return Err(CompileError::UnsupportedExpression(
2000                    "complex callback parameter".into(),
2001                ));
2002            };
2003
2004            // Compile body
2005            let body = match &*arrow.body {
2006                BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2007                BlockStmtOrExpr::BlockStmt(block) => {
2008                    // For block bodies, collect variable bindings and find return statement
2009                    let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
2010                    let mut return_expr: Option<ValueExpr> = None;
2011
2012                    for stmt in &block.stmts {
2013                        match stmt {
2014                            // Variable declaration: const x = ...; or let x = ...;
2015                            Stmt::Decl(Decl::Var(var_decl)) => {
2016                                for decl in &var_decl.decls {
2017                                    let var_name = self.get_var_name(&decl.name)?;
2018                                    if let Some(init) = &decl.init {
2019                                        let expr = self.compile_expr(init)?;
2020                                        bindings.push((var_name, expr));
2021                                    }
2022                                }
2023                            },
2024                            // Return statement
2025                            Stmt::Return(ret) => {
2026                                if let Some(arg) = &ret.arg {
2027                                    return_expr = Some(self.compile_expr(arg)?);
2028                                }
2029                                break; // Stop processing after return
2030                            },
2031                            // Expression statement (e.g., a side effect)
2032                            Stmt::Expr(_) => {
2033                                // Ignore expression statements in arrow body for now
2034                            },
2035                            _ => {},
2036                        }
2037                    }
2038
2039                    match return_expr {
2040                        Some(result) if bindings.is_empty() => result,
2041                        Some(result) => ValueExpr::Block {
2042                            bindings,
2043                            result: Box::new(result),
2044                        },
2045                        None => {
2046                            return Err(CompileError::UnsupportedExpression(
2047                                "callback block without return".into(),
2048                            ));
2049                        },
2050                    }
2051                },
2052            };
2053
2054            Ok((param_name, body))
2055        } else {
2056            Err(CompileError::UnsupportedExpression(
2057                "callback must be arrow function".into(),
2058            ))
2059        }
2060    }
2061
2062    /// Extract reduce callback: (acc, item) => expr
2063    fn extract_reduce_callback(
2064        &mut self,
2065        call: &CallExpr,
2066    ) -> Result<(String, String, ValueExpr), CompileError> {
2067        let arg = call
2068            .args
2069            .first()
2070            .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2071
2072        if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2073            // Reduce callback should have 2 parameters: (acc, item)
2074            if arrow.params.len() < 2 {
2075                return Err(CompileError::UnsupportedExpression(
2076                    "reduce callback must have 2 parameters".into(),
2077                ));
2078            }
2079
2080            let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
2081                ident.id.sym.to_string()
2082            } else {
2083                return Err(CompileError::UnsupportedExpression(
2084                    "complex callback parameter".into(),
2085                ));
2086            };
2087
2088            let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
2089                ident.id.sym.to_string()
2090            } else {
2091                return Err(CompileError::UnsupportedExpression(
2092                    "complex callback parameter".into(),
2093                ));
2094            };
2095
2096            // Compile body
2097            let body = match &*arrow.body {
2098                BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2099                BlockStmtOrExpr::BlockStmt(block) => {
2100                    // For block bodies, look for return statement
2101                    for stmt in &block.stmts {
2102                        if let Stmt::Return(ret) = stmt {
2103                            if let Some(arg) = &ret.arg {
2104                                return Ok((acc_name, item_name, self.compile_expr(arg)?));
2105                            }
2106                        }
2107                    }
2108                    return Err(CompileError::UnsupportedExpression(
2109                        "callback block without return".into(),
2110                    ));
2111                },
2112            };
2113
2114            Ok((acc_name, item_name, body))
2115        } else {
2116            Err(CompileError::UnsupportedExpression(
2117                "callback must be arrow function".into(),
2118            ))
2119        }
2120    }
2121
2122    fn extract_number_arg(
2123        &self,
2124        call: &CallExpr,
2125        index: usize,
2126    ) -> Result<Option<i64>, CompileError> {
2127        if let Some(arg) = call.args.get(index) {
2128            if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
2129                return Ok(Some(n.value as i64));
2130            }
2131            if let Expr::Unary(unary) = arg.expr.as_ref() {
2132                if unary.op == UnaryOp::Minus {
2133                    if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
2134                        return Ok(Some(-(n.value as i64)));
2135                    }
2136                }
2137            }
2138        }
2139        Ok(None)
2140    }
2141
2142    fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
2143        if let ValueExpr::ArrayMethod {
2144            method: ArrayMethodCall::Slice { end, .. },
2145            ..
2146        } = expr
2147        {
2148            return *end;
2149        }
2150        None
2151    }
2152
2153    fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
2154        match pat {
2155            Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
2156            _ => Err(CompileError::UnsupportedExpression(
2157                "complex destructuring".into(),
2158            )),
2159        }
2160    }
2161
2162    /// Generate a unique temp variable name for destructuring.
2163    fn next_temp_var(&mut self) -> String {
2164        let name = format!("__destructure_{}", self.destructure_counter);
2165        self.destructure_counter += 1;
2166        name
2167    }
2168
2169    /// Extract loop variable and destructuring steps for for-of loops.
2170    /// Returns (item_var, steps_to_prepend_to_body).
2171    fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
2172        match pat {
2173            Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
2174            Pat::Object(obj_pat) => {
2175                let temp_var = self.next_temp_var();
2176                let bindings = Self::extract_object_bindings(obj_pat)?;
2177                let steps = bindings
2178                    .into_iter()
2179                    .map(|(var_name, property)| PlanStep::Assign {
2180                        var: var_name,
2181                        expr: ValueExpr::PropertyAccess {
2182                            object: Box::new(ValueExpr::Variable(temp_var.clone())),
2183                            property,
2184                        },
2185                    })
2186                    .collect();
2187                Ok((temp_var, steps))
2188            },
2189            Pat::Array(arr_pat) => {
2190                let temp_var = self.next_temp_var();
2191                let mut steps = Vec::new();
2192                for (i, elem) in arr_pat.elems.iter().enumerate() {
2193                    if let Some(p) = elem {
2194                        let var_name = self.get_var_name(p)?;
2195                        steps.push(PlanStep::Assign {
2196                            var: var_name,
2197                            expr: ValueExpr::ArrayIndex {
2198                                array: Box::new(ValueExpr::Variable(temp_var.clone())),
2199                                index: Box::new(ValueExpr::Literal(JsonValue::Number(
2200                                    (i as i64).into(),
2201                                ))),
2202                            },
2203                        });
2204                    }
2205                }
2206                Ok((temp_var, steps))
2207            },
2208            _ => Err(CompileError::UnsupportedExpression(
2209                "complex loop variable pattern".into(),
2210            )),
2211        }
2212    }
2213
2214    /// Extract (var_name, property_key) bindings from an object destructuring pattern.
2215    /// `{ a, b }` → [("a", "a"), ("b", "b")]
2216    /// `{ id: userId }` → [("userId", "id")]
2217    fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
2218        let mut bindings = Vec::new();
2219        for prop in &obj_pat.props {
2220            match prop {
2221                ObjectPatProp::Assign(assign) => {
2222                    // Shorthand: `{ x }` — reject `{ x = default }` explicitly
2223                    if assign.value.is_some() {
2224                        return Err(CompileError::UnsupportedExpression(
2225                            "default values in destructuring".into(),
2226                        ));
2227                    }
2228                    let name = assign.key.sym.to_string();
2229                    bindings.push((name.clone(), name));
2230                },
2231                ObjectPatProp::KeyValue(kv) => {
2232                    // Renamed: `{ id: userId }`
2233                    let key = match &kv.key {
2234                        PropName::Ident(ident) => ident.sym.to_string(),
2235                        PropName::Str(s) => s.value.to_string_lossy().into_owned(),
2236                        _ => {
2237                            return Err(CompileError::UnsupportedExpression(
2238                                "computed destructuring key".into(),
2239                            ));
2240                        },
2241                    };
2242                    let var_name = match kv.value.as_ref() {
2243                        Pat::Ident(ident) => ident.id.sym.to_string(),
2244                        _ => {
2245                            return Err(CompileError::UnsupportedExpression(
2246                                "nested destructuring".into(),
2247                            ));
2248                        },
2249                    };
2250                    bindings.push((var_name, key));
2251                },
2252                ObjectPatProp::Rest(_) => {
2253                    return Err(CompileError::UnsupportedExpression(
2254                        "rest pattern in destructuring".into(),
2255                    ));
2256                },
2257            }
2258        }
2259        Ok(bindings)
2260    }
2261
2262    /// Compile object destructuring: `const { a, b } = expr`
2263    /// Generates a temp var assignment + property access assignments.
2264    fn compile_object_destructuring(
2265        &mut self,
2266        obj_pat: &ObjectPat,
2267        init: &Expr,
2268        steps: &mut Vec<PlanStep>,
2269    ) -> Result<(), CompileError> {
2270        let bindings = Self::extract_object_bindings(obj_pat)?;
2271        let temp_var = self.next_temp_var();
2272
2273        // Compile the RHS into the temp var
2274        self.compile_var_init(&temp_var, init, steps)?;
2275
2276        // Generate property access assignments for each binding
2277        for (var_name, property) in bindings {
2278            steps.push(PlanStep::Assign {
2279                var: var_name,
2280                expr: ValueExpr::PropertyAccess {
2281                    object: Box::new(ValueExpr::Variable(temp_var.clone())),
2282                    property,
2283                },
2284            });
2285        }
2286        Ok(())
2287    }
2288
2289    /// Compile array destructuring: `const [a, b] = expr`
2290    /// Generates a temp var assignment + index access assignments.
2291    fn compile_array_destructuring(
2292        &mut self,
2293        arr_pat: &ArrayPat,
2294        init: &Expr,
2295        steps: &mut Vec<PlanStep>,
2296    ) -> Result<(), CompileError> {
2297        let temp_var = self.next_temp_var();
2298
2299        // Compile the RHS into the temp var
2300        self.compile_var_init(&temp_var, init, steps)?;
2301
2302        // Generate index access assignments for each element
2303        for (i, elem) in arr_pat.elems.iter().enumerate() {
2304            if let Some(pat) = elem {
2305                let var_name = self.get_var_name(pat)?;
2306                steps.push(PlanStep::Assign {
2307                    var: var_name,
2308                    expr: ValueExpr::ArrayIndex {
2309                        array: Box::new(ValueExpr::Variable(temp_var.clone())),
2310                        index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
2311                    },
2312                });
2313            }
2314            // None elements (holes) are skipped: `const [, b] = arr`
2315        }
2316        Ok(())
2317    }
2318
2319    fn lit_to_json(&self, lit: &Lit) -> JsonValue {
2320        match lit {
2321            Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
2322            Lit::Num(n) => {
2323                if n.value.fract() == 0.0 {
2324                    JsonValue::Number((n.value as i64).into())
2325                } else {
2326                    serde_json::Number::from_f64(n.value)
2327                        .map(JsonValue::Number)
2328                        .unwrap_or(JsonValue::Null)
2329                }
2330            },
2331            Lit::Bool(b) => JsonValue::Bool(b.value),
2332            Lit::Null(_) => JsonValue::Null,
2333            _ => JsonValue::Null,
2334        }
2335    }
2336
2337    fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
2338        match prop {
2339            PropName::Ident(ident) => Ok(ident.sym.to_string()),
2340            PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
2341            PropName::Num(n) => Ok(n.value.to_string()),
2342            _ => Err(CompileError::UnsupportedExpression(
2343                "computed property".into(),
2344            )),
2345        }
2346    }
2347
2348    fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
2349        match op {
2350            BinaryOp::Add => Ok(BinaryOperator::Add),
2351            BinaryOp::Sub => Ok(BinaryOperator::Sub),
2352            BinaryOp::Mul => Ok(BinaryOperator::Mul),
2353            BinaryOp::Div => Ok(BinaryOperator::Div),
2354            BinaryOp::Mod => Ok(BinaryOperator::Mod),
2355            BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
2356            BinaryOp::EqEq => Ok(BinaryOperator::Eq),
2357            BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
2358            BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
2359            BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
2360            BinaryOp::Lt => Ok(BinaryOperator::Lt),
2361            BinaryOp::LtEq => Ok(BinaryOperator::Lte),
2362            BinaryOp::Gt => Ok(BinaryOperator::Gt),
2363            BinaryOp::GtEq => Ok(BinaryOperator::Gte),
2364            BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
2365            BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
2366            BinaryOp::NullishCoalescing => {
2367                // Handled separately as NullishCoalesce expr
2368                Err(CompileError::UnsupportedExpression(
2369                    "nullish coalescing".into(),
2370                ))
2371            },
2372            _ => Err(CompileError::UnsupportedExpression(format!(
2373                "binary operator {:?}",
2374                op
2375            ))),
2376        }
2377    }
2378
2379    fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
2380        self.api_call_count += 1;
2381
2382        // Track methods used
2383        if !self.methods_used.contains(&method.to_string()) {
2384            self.methods_used.push(method.to_string());
2385        }
2386
2387        // Track if mutations
2388        if method != "GET" && method != "HEAD" && method != "OPTIONS" {
2389            self.has_mutations = true;
2390        }
2391
2392        // Track endpoints (simplified path for static paths)
2393        let endpoint = if !path.is_dynamic() {
2394            path.parts
2395                .iter()
2396                .filter_map(|p| match p {
2397                    PathPart::Literal(s) => Some(s.clone()),
2398                    _ => None,
2399                })
2400                .collect::<String>()
2401        } else {
2402            "{dynamic}".to_string()
2403        };
2404        if !self.endpoints.contains(&endpoint) {
2405            self.endpoints.push(endpoint);
2406        }
2407    }
2408}
2409
2410impl Default for PlanCompiler {
2411    fn default() -> Self {
2412        Self::new()
2413    }
2414}
2415
2416// ============================================================================
2417// PLAN EXECUTOR - Executes the compiled execution plan
2418// ============================================================================
2419
2420/// Trait for making HTTP requests during execution.
2421///
2422/// This abstraction allows the executor to be used with different HTTP clients
2423/// and enables easy testing with mock implementations.
2424#[async_trait::async_trait]
2425pub trait HttpExecutor: Send + Sync {
2426    /// Execute an HTTP request.
2427    async fn execute_request(
2428        &self,
2429        method: &str,
2430        path: &str,
2431        body: Option<JsonValue>,
2432    ) -> Result<JsonValue, ExecutionError>;
2433}
2434
2435/// Executor for MCP foundation server calls.
2436///
2437/// Analogous to `HttpExecutor` for API calls, this trait abstracts MCP tool
2438/// invocation for use in the AST-based executor. Implementations delegate to
2439/// actual foundation clients (e.g., `CompositionClient`).
2440#[cfg(feature = "mcp-code-mode")]
2441#[async_trait::async_trait]
2442pub trait McpExecutor: Send + Sync {
2443    /// Call a tool on a foundation server.
2444    async fn call_tool(
2445        &self,
2446        server_id: &str,
2447        tool_name: &str,
2448        args: JsonValue,
2449    ) -> Result<JsonValue, ExecutionError>;
2450}
2451
2452/// Executor for SDK-backed API calls.
2453///
2454/// Analogous to `HttpExecutor` for HTTP calls, this trait abstracts named SDK
2455/// operation invocation. Implementations route `api.<operation>(args)` to actual
2456/// SDK calls (e.g., AWS Cost Explorer).
2457#[async_trait::async_trait]
2458pub trait SdkExecutor: Send + Sync {
2459    /// Execute a named SDK operation.
2460    ///
2461    /// `operation` is the camelCase method name (e.g., "getCostAndUsage").
2462    /// `args` is the optional JSON object argument from the script.
2463    async fn execute_operation(
2464        &self,
2465        operation: &str,
2466        args: Option<JsonValue>,
2467    ) -> Result<JsonValue, ExecutionError>;
2468}
2469
2470// ============================================================================
2471// MOCK HTTP EXECUTOR - For dry-run, testing, and development
2472// ============================================================================
2473
2474/// Execution mode for the mock executor.
2475#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2476pub enum MockExecutionMode {
2477    /// Dry-run: Returns mock responses, records calls for preview.
2478    #[default]
2479    DryRun,
2480    /// Testing: Returns configured mock responses for assertions.
2481    Testing,
2482    /// Record: Passes through to a real executor and records responses.
2483    Record,
2484}
2485
2486/// Mock HTTP executor for dry-run validation and testing.
2487///
2488/// This executor doesn't make real HTTP calls. Instead, it:
2489/// - Records all API calls that would be made
2490/// - Returns configurable mock responses
2491/// - Enables dry-run validation showing what a script would do
2492///
2493/// # Example
2494///
2495/// ```ignore
2496/// use mcp_server_common::code_mode::executor::{MockHttpExecutor, PlanExecutor};
2497///
2498/// // Create a mock executor for dry-run
2499/// let mock = MockHttpExecutor::new_dry_run();
2500///
2501/// // Or with custom responses for testing
2502/// let mock = MockHttpExecutor::new_testing()
2503///     .with_response("/users", json!({"users": [{"id": 1, "name": "Test"}]}))
2504///     .with_response("/orders/*", json!({"orders": []}));
2505///
2506/// // Execute the plan
2507/// let executor = PlanExecutor::new(mock, config);
2508/// let result = executor.execute(plan).await?;
2509///
2510/// // Check what calls would be made
2511/// for call in mock.recorded_calls() {
2512///     println!("Would call: {} {}", call.method, call.path);
2513/// }
2514/// ```
2515pub struct MockHttpExecutor {
2516    /// Mock responses by path pattern (exact match or glob pattern with *)
2517    responses: std::sync::RwLock<HashMap<String, JsonValue>>,
2518    /// Default response for unmatched paths
2519    default_response: JsonValue,
2520    /// Record of all calls made (method, path, body, response)
2521    recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
2522}
2523
2524/// A recorded mock call with request and response.
2525#[derive(Debug, Clone, Serialize)]
2526pub struct MockedCall {
2527    /// HTTP method (GET, POST, etc.)
2528    pub method: String,
2529    /// Request path
2530    pub path: String,
2531    /// Request body if any
2532    pub body: Option<JsonValue>,
2533    /// Response returned
2534    pub response: JsonValue,
2535}
2536
2537impl MockHttpExecutor {
2538    /// Create a new mock executor for dry-run mode.
2539    /// Returns empty objects `{}` for all calls.
2540    pub fn new_dry_run() -> Self {
2541        Self {
2542            responses: std::sync::RwLock::new(HashMap::new()),
2543            default_response: JsonValue::Object(serde_json::Map::new()),
2544            recorded_calls: std::sync::RwLock::new(Vec::new()),
2545        }
2546    }
2547
2548    /// Create a new mock executor for testing mode.
2549    /// Configure responses with `with_response()`.
2550    pub fn new_testing() -> Self {
2551        Self {
2552            responses: std::sync::RwLock::new(HashMap::new()),
2553            default_response: JsonValue::Object(serde_json::Map::new()),
2554            recorded_calls: std::sync::RwLock::new(Vec::new()),
2555        }
2556    }
2557
2558    /// Set the default response for unmatched paths.
2559    pub fn with_default_response(mut self, response: JsonValue) -> Self {
2560        self.default_response = response;
2561        self
2562    }
2563
2564    /// Add a mock response for a specific path pattern.
2565    /// Supports exact matches and simple glob patterns with `*`.
2566    ///
2567    /// # Examples
2568    ///
2569    /// ```ignore
2570    /// mock.with_response("/users", json!({"users": []}))
2571    ///     .with_response("/users/*", json!({"id": 1, "name": "Test"}))
2572    ///     .with_response("/orders/*/items", json!({"items": []}));
2573    /// ```
2574    pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
2575        self.responses
2576            .write()
2577            .unwrap()
2578            .insert(path_pattern.to_string(), response);
2579        self
2580    }
2581
2582    /// Add a mock response (non-builder version).
2583    pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
2584        self.responses
2585            .write()
2586            .unwrap()
2587            .insert(path_pattern.to_string(), response);
2588    }
2589
2590    /// Get all recorded calls.
2591    pub fn recorded_calls(&self) -> Vec<MockedCall> {
2592        self.recorded_calls.read().unwrap().clone()
2593    }
2594
2595    /// Clear all recorded calls.
2596    pub fn clear_calls(&self) {
2597        self.recorded_calls.write().unwrap().clear();
2598    }
2599
2600    /// Get the number of calls made.
2601    pub fn call_count(&self) -> usize {
2602        self.recorded_calls.read().unwrap().len()
2603    }
2604
2605    /// Check if a specific path was called.
2606    pub fn was_called(&self, path: &str) -> bool {
2607        self.recorded_calls
2608            .read()
2609            .unwrap()
2610            .iter()
2611            .any(|c| c.path == path)
2612    }
2613
2614    /// Check if a path was called with a specific method.
2615    pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
2616        self.recorded_calls
2617            .read()
2618            .unwrap()
2619            .iter()
2620            .any(|c| c.method == method && c.path == path)
2621    }
2622
2623    /// Find the response for a path, checking patterns.
2624    fn find_response(&self, path: &str) -> JsonValue {
2625        let responses = self.responses.read().unwrap();
2626
2627        // First try exact match
2628        if let Some(response) = responses.get(path) {
2629            return response.clone();
2630        }
2631
2632        // Then try pattern matching
2633        for (pattern, response) in responses.iter() {
2634            if Self::matches_pattern(pattern, path) {
2635                return response.clone();
2636            }
2637        }
2638
2639        // Return default
2640        self.default_response.clone()
2641    }
2642
2643    /// Simple glob pattern matching (supports * as wildcard for path segments).
2644    fn matches_pattern(pattern: &str, path: &str) -> bool {
2645        if !pattern.contains('*') {
2646            return pattern == path;
2647        }
2648
2649        let pattern_parts: Vec<&str> = pattern.split('/').collect();
2650        let path_parts: Vec<&str> = path.split('/').collect();
2651
2652        if pattern_parts.len() != path_parts.len() {
2653            // Check for trailing * that matches multiple segments
2654            if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
2655                // Allow trailing wildcard to match remaining segments
2656            } else {
2657                return false;
2658            }
2659        }
2660
2661        for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
2662            if *p != "*" && *p != *s {
2663                return false;
2664            }
2665        }
2666
2667        true
2668    }
2669}
2670
2671#[async_trait::async_trait]
2672impl HttpExecutor for MockHttpExecutor {
2673    async fn execute_request(
2674        &self,
2675        method: &str,
2676        path: &str,
2677        body: Option<JsonValue>,
2678    ) -> Result<JsonValue, ExecutionError> {
2679        let response = self.find_response(path);
2680
2681        // Record the call
2682        let call = MockedCall {
2683            method: method.to_string(),
2684            path: path.to_string(),
2685            body,
2686            response: response.clone(),
2687        };
2688        self.recorded_calls.write().unwrap().push(call);
2689
2690        Ok(response)
2691    }
2692}
2693
2694// Implement Send + Sync (safe because we use RwLock)
2695unsafe impl Send for MockHttpExecutor {}
2696unsafe impl Sync for MockHttpExecutor {}
2697
2698/// Result of executing a plan.
2699#[derive(Debug, Clone, Serialize)]
2700pub struct ExecutionResult {
2701    /// The final return value
2702    pub value: JsonValue,
2703    /// Log of all API calls made
2704    pub api_calls: Vec<ApiCallLog>,
2705    /// Total execution time in milliseconds
2706    pub execution_time_ms: u64,
2707}
2708
2709/// Log entry for an API call.
2710#[derive(Debug, Clone, Serialize)]
2711pub struct ApiCallLog {
2712    /// HTTP method
2713    pub method: String,
2714    /// Resolved path
2715    pub path: String,
2716    /// Request body (if any)
2717    pub body: Option<JsonValue>,
2718    /// Response value
2719    pub response: JsonValue,
2720    /// Time taken in milliseconds
2721    pub duration_ms: u64,
2722}
2723
2724/// Executes a compiled execution plan.
2725pub struct PlanExecutor<H: HttpExecutor> {
2726    http: H,
2727    config: ExecutionConfig,
2728    variables: HashMap<String, JsonValue>,
2729    api_calls: Vec<ApiCallLog>,
2730    api_call_count: usize,
2731    #[cfg(feature = "mcp-code-mode")]
2732    mcp: Option<Box<dyn McpExecutor>>,
2733    /// Optional SDK executor for SDK-backed servers (e.g., aws-billing).
2734    sdk: Option<Box<dyn SdkExecutor>>,
2735}
2736
2737impl<H: HttpExecutor> PlanExecutor<H> {
2738    /// Create a new executor with the given HTTP client.
2739    pub fn new(http: H, config: ExecutionConfig) -> Self {
2740        Self {
2741            http,
2742            config,
2743            variables: HashMap::new(),
2744            api_calls: Vec::new(),
2745            api_call_count: 0,
2746            #[cfg(feature = "mcp-code-mode")]
2747            mcp: None,
2748            sdk: None,
2749        }
2750    }
2751
2752    /// Set the MCP executor for foundation server calls.
2753    #[cfg(feature = "mcp-code-mode")]
2754    pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
2755        self.mcp = Some(Box::new(executor));
2756    }
2757
2758    /// Set the SDK executor for SDK-backed servers.
2759    pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
2760        self.sdk = Some(Box::new(executor));
2761    }
2762
2763    /// Pre-bind a variable before execution (e.g., `args` for script tools).
2764    pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
2765        self.variables.insert(name.into(), value);
2766    }
2767
2768    /// Execute a plan and return the result.
2769    pub async fn execute(
2770        &mut self,
2771        plan: &ExecutionPlan,
2772    ) -> Result<ExecutionResult, ExecutionError> {
2773        let start = std::time::Instant::now();
2774
2775        let mut return_value = JsonValue::Null;
2776
2777        for step in &plan.steps {
2778            match self.execute_step(step).await? {
2779                StepOutcome::Return(value) => {
2780                    return_value = value;
2781                    break; // Early return — stop executing further steps
2782                },
2783                StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
2784            }
2785        }
2786
2787        // Validate output against output blocklist.
2788        // These are fields that can be used internally but cannot be returned.
2789        let blocked_in_output =
2790            find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
2791
2792        if !blocked_in_output.is_empty() {
2793            return Err(ExecutionError::RuntimeError {
2794                message: format!(
2795                    "Script output contains blocked fields: {}",
2796                    blocked_in_output.join(", ")
2797                ),
2798            });
2799        }
2800
2801        Ok(ExecutionResult {
2802            value: return_value,
2803            api_calls: std::mem::take(&mut self.api_calls),
2804            execution_time_ms: start.elapsed().as_millis() as u64,
2805        })
2806    }
2807
2808    /// Execute a single step, returning a `StepOutcome` for control flow.
2809    /// Uses Box::pin for recursive calls to avoid infinite future size.
2810    fn execute_step<'a>(
2811        &'a mut self,
2812        step: &'a PlanStep,
2813    ) -> std::pin::Pin<
2814        Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
2815    > {
2816        Box::pin(async move {
2817            match step {
2818                PlanStep::ApiCall {
2819                    result_var,
2820                    method,
2821                    path,
2822                    body,
2823                } => {
2824                    self.api_call_count += 1;
2825                    if self.api_call_count > self.config.max_api_calls {
2826                        return Err(ExecutionError::RuntimeError {
2827                            message: format!(
2828                                "Too many API calls: {} (max: {})",
2829                                self.api_call_count, self.config.max_api_calls
2830                            ),
2831                        });
2832                    }
2833
2834                    let resolved_path = self.resolve_path(path)?;
2835                    let resolved_body = match body {
2836                        Some(expr) => Some(self.evaluate(expr)?),
2837                        None => None,
2838                    };
2839
2840                    let call_start = std::time::Instant::now();
2841                    let raw_response = self
2842                        .http
2843                        .execute_request(method, &resolved_path, resolved_body.clone())
2844                        .await
2845                        .map_err(|e| ExecutionError::RuntimeError {
2846                            message: format!("{} {} failed: {}", method, resolved_path, e),
2847                        })?;
2848                    let duration_ms = call_start.elapsed().as_millis() as u64;
2849
2850                    // Filter blocked fields from API response before scripts can access them.
2851                    // This implements the "internal blocklist" - fields that are never accessible.
2852                    let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
2853
2854                    self.api_calls.push(ApiCallLog {
2855                        method: method.clone(),
2856                        path: resolved_path,
2857                        body: resolved_body,
2858                        response: response.clone(),
2859                        duration_ms,
2860                    });
2861
2862                    if result_var != "_" {
2863                        self.variables.insert(result_var.clone(), response);
2864                    }
2865                    Ok(StepOutcome::None)
2866                },
2867
2868                PlanStep::Assign { var, expr } => {
2869                    let value = self.evaluate(expr)?;
2870                    self.variables.insert(var.clone(), value);
2871                    Ok(StepOutcome::None)
2872                },
2873
2874                PlanStep::Conditional {
2875                    condition,
2876                    then_steps,
2877                    else_steps,
2878                } => {
2879                    let cond_value = self.evaluate(condition)?;
2880                    let steps = if shared_is_truthy(&cond_value) {
2881                        then_steps
2882                    } else {
2883                        else_steps
2884                    };
2885
2886                    for step in steps {
2887                        match self.execute_step(step).await? {
2888                            StepOutcome::None => {},
2889                            outcome => return Ok(outcome),
2890                        }
2891                    }
2892                    Ok(StepOutcome::None)
2893                },
2894
2895                PlanStep::BoundedLoop {
2896                    item_var,
2897                    collection,
2898                    max_iterations,
2899                    body,
2900                } => {
2901                    let collection_value = self.evaluate(collection)?;
2902                    let items = match collection_value {
2903                        JsonValue::Array(arr) => arr,
2904                        _ => {
2905                            return Err(ExecutionError::RuntimeError {
2906                                message: "Loop collection must be an array".into(),
2907                            })
2908                        },
2909                    };
2910
2911                    let limit = (*max_iterations).min(self.config.max_loop_iterations);
2912                    'outer: for item in items.into_iter().take(limit) {
2913                        self.variables.insert(item_var.clone(), item);
2914
2915                        for step in body {
2916                            match self.execute_step(step).await? {
2917                                StepOutcome::Return(value) => {
2918                                    return Ok(StepOutcome::Return(value))
2919                                },
2920                                StepOutcome::None => {},
2921                                StepOutcome::Continue => continue 'outer,
2922                                StepOutcome::Break => break 'outer,
2923                            }
2924                        }
2925                    }
2926                    Ok(StepOutcome::None)
2927                },
2928
2929                PlanStep::Return { value } => {
2930                    let result = self.evaluate(value)?;
2931                    Ok(StepOutcome::Return(result))
2932                },
2933
2934                PlanStep::TryCatch {
2935                    try_steps,
2936                    catch_var,
2937                    catch_steps,
2938                    finally_steps,
2939                } => {
2940                    // Execute try block
2941                    let try_result = async {
2942                        for step in try_steps {
2943                            match self.execute_step(step).await? {
2944                                StepOutcome::None => {},
2945                                outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
2946                            }
2947                        }
2948                        Ok(StepOutcome::None)
2949                    }
2950                    .await;
2951
2952                    // If try succeeded, just run finally
2953                    let result = match try_result {
2954                        Ok(outcome) => {
2955                            // Try block succeeded
2956                            outcome
2957                        },
2958                        Err(error) => {
2959                            // Try block failed, run catch
2960                            if let Some(var) = catch_var {
2961                                // Store the error in the catch variable
2962                                let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
2963                                    "message".to_string(),
2964                                    JsonValue::String(format!("{}", error)),
2965                                )]));
2966                                self.variables.insert(var.clone(), error_obj);
2967                            }
2968
2969                            // Execute catch block
2970                            let mut catch_outcome = StepOutcome::None;
2971                            for step in catch_steps {
2972                                match self.execute_step(step).await? {
2973                                    StepOutcome::None => {},
2974                                    outcome => {
2975                                        catch_outcome = outcome;
2976                                        break;
2977                                    },
2978                                }
2979                            }
2980                            catch_outcome
2981                        },
2982                    };
2983
2984                    // Execute finally block (always runs)
2985                    for step in finally_steps {
2986                        match self.execute_step(step).await? {
2987                            StepOutcome::None => {},
2988                            outcome => return Ok(outcome),
2989                        }
2990                    }
2991
2992                    Ok(result)
2993                },
2994
2995                // Parallel API calls: await Promise.all([api.get(...), ...])
2996                // Executed sequentially (true parallelism isn't needed for correctness),
2997                // results collected into an array assigned to result_var.
2998                PlanStep::ParallelApiCalls { result_var, calls } => {
2999                    let mut results = Vec::with_capacity(calls.len());
3000                    for (_temp_var, method, path, body) in calls {
3001                        self.api_call_count += 1;
3002                        if self.api_call_count > self.config.max_api_calls {
3003                            return Err(ExecutionError::RuntimeError {
3004                                message: format!(
3005                                    "Maximum API calls exceeded ({})",
3006                                    self.config.max_api_calls
3007                                ),
3008                            });
3009                        }
3010
3011                        let resolved_path = self.resolve_path(path)?;
3012                        let resolved_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
3013                        let call_start = std::time::Instant::now();
3014                        let raw_response = self
3015                            .http
3016                            .execute_request(method, &resolved_path, resolved_body.clone())
3017                            .await
3018                            .map_err(|e| ExecutionError::RuntimeError {
3019                                message: format!("{} {} failed: {}", method, resolved_path, e),
3020                            })?;
3021                        let duration_ms = call_start.elapsed().as_millis() as u64;
3022                        let response =
3023                            filter_blocked_fields(raw_response, &self.config.blocked_fields);
3024
3025                        self.api_calls.push(ApiCallLog {
3026                            method: method.clone(),
3027                            path: resolved_path,
3028                            body: resolved_body,
3029                            response: response.clone(),
3030                            duration_ms,
3031                        });
3032
3033                        results.push(response);
3034                    }
3035                    self.variables
3036                        .insert(result_var.clone(), JsonValue::Array(results));
3037                    Ok(StepOutcome::None)
3038                },
3039
3040                // Continue: signal to skip to next loop iteration
3041                PlanStep::Continue => Ok(StepOutcome::Continue),
3042
3043                // Break: signal to exit the current loop
3044                PlanStep::Break => Ok(StepOutcome::Break),
3045
3046                // MCP tool call: await mcp.call('server', 'tool', { args })
3047                #[cfg(feature = "mcp-code-mode")]
3048                PlanStep::McpCall {
3049                    result_var,
3050                    server_id,
3051                    tool_name,
3052                    args,
3053                } => {
3054                    self.api_call_count += 1;
3055                    if self.api_call_count > self.config.max_api_calls {
3056                        return Err(ExecutionError::RuntimeError {
3057                            message: format!(
3058                                "Too many calls: {} (max: {})",
3059                                self.api_call_count, self.config.max_api_calls
3060                            ),
3061                        });
3062                    }
3063
3064                    let resolved_args = match args {
3065                        Some(expr) => self.evaluate(expr)?,
3066                        None => JsonValue::Object(Default::default()),
3067                    };
3068
3069                    let mcp_executor =
3070                        self.mcp
3071                            .as_ref()
3072                            .ok_or_else(|| ExecutionError::RuntimeError {
3073                                message: "MCP executor not configured".into(),
3074                            })?;
3075
3076                    let call_start = std::time::Instant::now();
3077                    let result = mcp_executor
3078                        .call_tool(server_id, tool_name, resolved_args.clone())
3079                        .await?;
3080                    let duration_ms = call_start.elapsed().as_millis() as u64;
3081
3082                    self.api_calls.push(ApiCallLog {
3083                        method: format!("MCP:{}.{}", server_id, tool_name),
3084                        path: format!("{}/{}", server_id, tool_name),
3085                        body: Some(resolved_args),
3086                        response: result.clone(),
3087                        duration_ms,
3088                    });
3089
3090                    if result_var != "_" {
3091                        self.variables.insert(result_var.clone(), result);
3092                    }
3093                    Ok(StepOutcome::None)
3094                },
3095
3096                // SDK call: await api.getCostAndUsage({ ... })
3097                PlanStep::SdkCall {
3098                    result_var,
3099                    operation,
3100                    args,
3101                } => {
3102                    self.api_call_count += 1;
3103                    if self.api_call_count > self.config.max_api_calls {
3104                        return Err(ExecutionError::RuntimeError {
3105                            message: format!(
3106                                "Too many calls: {} (max: {})",
3107                                self.api_call_count, self.config.max_api_calls
3108                            ),
3109                        });
3110                    }
3111
3112                    let resolved_args =
3113                        args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
3114
3115                    let sdk_executor =
3116                        self.sdk
3117                            .as_ref()
3118                            .ok_or_else(|| ExecutionError::RuntimeError {
3119                                message: "SDK executor not configured".into(),
3120                            })?;
3121
3122                    let call_start = std::time::Instant::now();
3123                    let result = sdk_executor
3124                        .execute_operation(operation, resolved_args.clone())
3125                        .await?;
3126                    let duration_ms = call_start.elapsed().as_millis() as u64;
3127
3128                    self.api_calls.push(ApiCallLog {
3129                        method: operation.clone(),
3130                        path: format!("sdk:{}", operation),
3131                        body: resolved_args,
3132                        response: result.clone(),
3133                        duration_ms,
3134                    });
3135
3136                    if result_var != "_" {
3137                        self.variables.insert(result_var.clone(), result);
3138                    }
3139                    Ok(StepOutcome::None)
3140                },
3141            }
3142        })
3143    }
3144
3145    /// Resolve a path template to a concrete path string.
3146    fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
3147        let mut result = String::new();
3148        for part in &path.parts {
3149            match part {
3150                PathPart::Literal(s) => result.push_str(s),
3151                PathPart::Variable(var) => {
3152                    let value =
3153                        self.variables
3154                            .get(var)
3155                            .ok_or_else(|| ExecutionError::RuntimeError {
3156                                message: format!("Undefined variable in path: {}", var),
3157                            })?;
3158                    result.push_str(&shared_json_to_string_with_mode(
3159                        value,
3160                        JsonStringMode::Json,
3161                    ));
3162                },
3163                PathPart::Expression(expr) => {
3164                    let value = self.evaluate(expr)?;
3165                    result.push_str(&shared_json_to_string_with_mode(
3166                        &value,
3167                        JsonStringMode::Json,
3168                    ));
3169                },
3170            }
3171        }
3172        Ok(result)
3173    }
3174
3175    /// Evaluate an expression to a JSON value.
3176    /// Delegates to the shared evaluation module.
3177    fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
3178        shared_evaluate(expr, &self.variables)
3179    }
3180}
3181
3182// ============================================================================
3183// LEGACY COMPATIBILITY - Types for backward compatibility
3184// ============================================================================
3185
3186/// Legacy JsExecutor type alias for backward compatibility.
3187pub type JsExecutor = PlanCompiler;
3188
3189#[cfg(test)]
3190mod tests {
3191    use super::*;
3192
3193    #[test]
3194    fn test_execution_config_default() {
3195        let config = ExecutionConfig::default();
3196        assert_eq!(config.max_api_calls, 50);
3197        assert_eq!(config.timeout_seconds, 30);
3198        assert_eq!(config.max_loop_iterations, 100);
3199    }
3200
3201    #[test]
3202    fn test_path_template_static() {
3203        let path = PathTemplate::static_path("/users".into());
3204        assert!(!path.is_dynamic());
3205    }
3206
3207    #[test]
3208    fn test_path_template_dynamic() {
3209        let path = PathTemplate {
3210            parts: vec![
3211                PathPart::Literal("/users/".into()),
3212                PathPart::Variable("id".into()),
3213            ],
3214        };
3215        assert!(path.is_dynamic());
3216    }
3217
3218    #[test]
3219    fn test_plan_metadata() {
3220        let metadata = PlanMetadata {
3221            api_call_count: 2,
3222            has_mutations: false,
3223            endpoints: vec!["/users".into(), "/products".into()],
3224            methods_used: vec!["GET".into()],
3225        };
3226        assert_eq!(metadata.api_call_count, 2);
3227        assert!(!metadata.has_mutations);
3228    }
3229
3230    #[test]
3231    fn test_compile_simple_api_call() {
3232        let code = r#"
3233            const user = await api.get('/users/1');
3234            return user;
3235        "#;
3236
3237        let mut compiler = PlanCompiler::new();
3238        let plan = compiler.compile_code(code).expect("Should compile");
3239
3240        assert_eq!(plan.metadata.api_call_count, 1);
3241        assert!(!plan.metadata.has_mutations);
3242        assert_eq!(plan.steps.len(), 2); // ApiCall + Return
3243    }
3244
3245    #[test]
3246    fn test_compile_multiple_api_calls() {
3247        let code = r#"
3248            const users = await api.get('/users');
3249            const products = await api.get('/products');
3250            return { users, products };
3251        "#;
3252
3253        let mut compiler = PlanCompiler::new();
3254        let plan = compiler.compile_code(code).expect("Should compile");
3255
3256        assert_eq!(plan.metadata.api_call_count, 2);
3257        assert!(!plan.metadata.has_mutations);
3258    }
3259
3260    #[test]
3261    fn test_compile_mutation() {
3262        let code = r#"
3263            const result = await api.post('/users', { name: 'Test' });
3264            return result;
3265        "#;
3266
3267        let mut compiler = PlanCompiler::new();
3268        let plan = compiler.compile_code(code).expect("Should compile");
3269
3270        assert_eq!(plan.metadata.api_call_count, 1);
3271        assert!(plan.metadata.has_mutations);
3272    }
3273
3274    #[test]
3275    fn test_compile_dynamic_path() {
3276        let code = r#"
3277            const id = 123;
3278            const user = await api.get(`/users/${id}`);
3279            return user;
3280        "#;
3281
3282        let mut compiler = PlanCompiler::new();
3283        let plan = compiler.compile_code(code).expect("Should compile");
3284
3285        assert_eq!(plan.metadata.api_call_count, 1);
3286    }
3287
3288    #[test]
3289    fn test_compile_bounded_loop() {
3290        let code = r#"
3291            const items = [];
3292            const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3293            for (const user of users.slice(0, 2)) {
3294                const detail = await api.get(`/users/${user.id}`);
3295                items.push(detail);
3296            }
3297            return items;
3298        "#;
3299
3300        let mut compiler = PlanCompiler::new();
3301        let plan = compiler.compile_code(code).expect("Should compile");
3302
3303        // The loop is bounded, so it should compile
3304        assert!(plan
3305            .steps
3306            .iter()
3307            .any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
3308    }
3309
3310    #[test]
3311    fn test_compile_unbounded_loop_detection() {
3312        // Note: The current compiler allows for-of loops without explicit .slice() bounds
3313        // as long as the loop body doesn't exceed iteration limits at runtime.
3314        // This test documents the current behavior.
3315        let code = r#"
3316            const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3317            for (const user of users) {
3318                const detail = await api.get(`/users/${user.id}`);
3319            }
3320            return users;
3321        "#;
3322
3323        let mut compiler = PlanCompiler::new();
3324        let result = compiler.compile_code(code);
3325
3326        // Currently this compiles - runtime will enforce iteration limits
3327        // See #249 — investigate compile-time loop-bounds checking.
3328        assert!(result.is_ok(), "Loop compiled: {:?}", result);
3329    }
3330
3331    #[test]
3332    fn test_compile_conditional() {
3333        let code = r#"
3334            const user = await api.get('/users/1');
3335            if (user.active) {
3336                const orders = await api.get(`/users/${user.id}/orders`);
3337                return orders;
3338            } else {
3339                return [];
3340            }
3341        "#;
3342
3343        let mut compiler = PlanCompiler::new();
3344        let plan = compiler.compile_code(code).expect("Should compile");
3345
3346        assert!(plan
3347            .steps
3348            .iter()
3349            .any(|s| matches!(s, PlanStep::Conditional { .. })));
3350    }
3351
3352    // Mock HTTP executor for testing
3353    struct MockHttpExecutor {
3354        responses: std::collections::HashMap<String, JsonValue>,
3355    }
3356
3357    impl MockHttpExecutor {
3358        fn new() -> Self {
3359            Self {
3360                responses: std::collections::HashMap::new(),
3361            }
3362        }
3363
3364        fn add_response(&mut self, path: &str, response: JsonValue) {
3365            self.responses.insert(path.to_string(), response);
3366        }
3367    }
3368
3369    #[async_trait::async_trait]
3370    impl HttpExecutor for MockHttpExecutor {
3371        async fn execute_request(
3372            &self,
3373            _method: &str,
3374            path: &str,
3375            _body: Option<JsonValue>,
3376        ) -> Result<JsonValue, ExecutionError> {
3377            self.responses
3378                .get(path)
3379                .cloned()
3380                .ok_or_else(|| ExecutionError::RuntimeError {
3381                    message: format!("No mock response for path: {}", path),
3382                })
3383        }
3384    }
3385
3386    #[tokio::test]
3387    async fn test_execute_simple_api_call() {
3388        let code = r#"
3389            const user = await api.get('/users/1');
3390            return user;
3391        "#;
3392
3393        let mut compiler = PlanCompiler::new();
3394        let plan = compiler.compile_code(code).expect("Should compile");
3395
3396        let mut mock_http = MockHttpExecutor::new();
3397        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
3398
3399        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3400        let result = executor.execute(&plan).await.expect("Should execute");
3401
3402        assert_eq!(result.value["id"], 1);
3403        assert_eq!(result.value["name"], "Alice");
3404        assert_eq!(result.api_calls.len(), 1);
3405    }
3406
3407    #[tokio::test]
3408    async fn test_execute_multiple_api_calls() {
3409        let code = r#"
3410            const users = await api.get('/users');
3411            const products = await api.get('/products');
3412            return { users, products };
3413        "#;
3414
3415        let mut compiler = PlanCompiler::new();
3416        let plan = compiler.compile_code(code).expect("Should compile");
3417
3418        let mut mock_http = MockHttpExecutor::new();
3419        mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
3420        mock_http.add_response(
3421            "/products",
3422            serde_json::json!([{ "id": 100, "name": "Widget" }]),
3423        );
3424
3425        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3426        let result = executor.execute(&plan).await.expect("Should execute");
3427
3428        assert!(result.value["users"].is_array());
3429        assert!(result.value["products"].is_array());
3430        assert_eq!(result.api_calls.len(), 2);
3431    }
3432
3433    #[tokio::test]
3434    async fn test_execute_with_template_path() {
3435        let code = r#"
3436            const userId = 42;
3437            const user = await api.get(`/users/${userId}`);
3438            return user;
3439        "#;
3440
3441        let mut compiler = PlanCompiler::new();
3442        let plan = compiler.compile_code(code).expect("Should compile");
3443
3444        let mut mock_http = MockHttpExecutor::new();
3445        mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
3446
3447        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3448        let result = executor.execute(&plan).await.expect("Should execute");
3449
3450        assert_eq!(result.value["id"], 42);
3451        assert_eq!(result.value["name"], "Bob");
3452    }
3453
3454    #[tokio::test]
3455    async fn test_execute_conditional_true_branch() {
3456        let code = r#"
3457            const user = await api.get('/users/1');
3458            if (user.active) {
3459                return { status: "active", user: user };
3460            } else {
3461                return { status: "inactive" };
3462            }
3463        "#;
3464
3465        let mut compiler = PlanCompiler::new();
3466        let plan = compiler.compile_code(code).expect("Should compile");
3467
3468        let mut mock_http = MockHttpExecutor::new();
3469        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
3470
3471        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3472        let result = executor.execute(&plan).await.expect("Should execute");
3473
3474        assert_eq!(result.value["status"], "active");
3475    }
3476
3477    #[tokio::test]
3478    async fn test_execute_conditional_false_branch() {
3479        let code = r#"
3480            const user = await api.get('/users/1');
3481            if (user.active) {
3482                return { status: "active" };
3483            } else {
3484                return { status: "inactive", user: user };
3485            }
3486        "#;
3487
3488        let mut compiler = PlanCompiler::new();
3489        let plan = compiler.compile_code(code).expect("Should compile");
3490
3491        let mut mock_http = MockHttpExecutor::new();
3492        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
3493
3494        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3495        let result = executor.execute(&plan).await.expect("Should execute");
3496
3497        assert_eq!(result.value["status"], "inactive");
3498    }
3499
3500    #[tokio::test]
3501    async fn test_compile_and_execute_reduce() {
3502        let code = r#"
3503            const products = await api.get('/products');
3504            const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3505            return { total: totalPrice };
3506        "#;
3507
3508        let mut compiler = PlanCompiler::new();
3509        let plan = compiler.compile_code(code).expect("Should compile reduce");
3510
3511        let mut mock_http = MockHttpExecutor::new();
3512        mock_http.add_response(
3513            "/products",
3514            serde_json::json!([
3515                { "id": 1, "name": "Widget", "price": 10 },
3516                { "id": 2, "name": "Gadget", "price": 25 },
3517                { "id": 3, "name": "Gizmo", "price": 15 }
3518            ]),
3519        );
3520
3521        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3522        let result = executor.execute(&plan).await.expect("Should execute");
3523
3524        // Result is f64, compare as number
3525        assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
3526    }
3527
3528    #[tokio::test]
3529    async fn test_compile_and_execute_to_fixed() {
3530        let code = r#"
3531            const products = await api.get('/products');
3532            const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3533            const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
3534            return { averagePrice: averagePrice.toFixed(2) };
3535        "#;
3536
3537        let mut compiler = PlanCompiler::new();
3538        let plan = compiler.compile_code(code).expect("Should compile toFixed");
3539
3540        let mut mock_http = MockHttpExecutor::new();
3541        mock_http.add_response(
3542            "/products",
3543            serde_json::json!([
3544                { "id": 1, "name": "Widget", "price": 10 },
3545                { "id": 2, "name": "Gadget", "price": 25 },
3546                { "id": 3, "name": "Gizmo", "price": 15 }
3547            ]),
3548        );
3549
3550        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3551        let result = executor.execute(&plan).await.expect("Should execute");
3552
3553        // 50 / 3 = 16.666... toFixed(2) = "16.67"
3554        assert_eq!(result.value["averagePrice"], "16.67");
3555    }
3556
3557    // =========================================================================
3558    // Field Filtering Tests
3559    // =========================================================================
3560
3561    #[test]
3562    fn test_filter_blocked_fields_simple() {
3563        let value = serde_json::json!({
3564            "id": 1,
3565            "name": "Alice",
3566            "password": "secret123",
3567            "email": "alice@example.com"
3568        });
3569
3570        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3571        let filtered = filter_blocked_fields(value, &blocked);
3572
3573        assert_eq!(filtered["id"], 1);
3574        assert_eq!(filtered["name"], "Alice");
3575        assert_eq!(filtered["email"], "alice@example.com");
3576        assert!(filtered.get("password").is_none());
3577    }
3578
3579    #[test]
3580    fn test_filter_blocked_fields_multiple() {
3581        let value = serde_json::json!({
3582            "id": 1,
3583            "name": "Alice",
3584            "password": "secret123",
3585            "ssn": "123-45-6789",
3586            "apiKey": "key-abc123"
3587        });
3588
3589        let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
3590            .iter()
3591            .map(|s| s.to_string())
3592            .collect();
3593        let filtered = filter_blocked_fields(value, &blocked);
3594
3595        assert_eq!(filtered["id"], 1);
3596        assert_eq!(filtered["name"], "Alice");
3597        assert!(filtered.get("password").is_none());
3598        assert!(filtered.get("ssn").is_none());
3599        assert!(filtered.get("apiKey").is_none());
3600    }
3601
3602    #[test]
3603    fn test_filter_blocked_fields_nested() {
3604        let value = serde_json::json!({
3605            "user": {
3606                "id": 1,
3607                "profile": {
3608                    "name": "Alice",
3609                    "password": "secret123"
3610                }
3611            }
3612        });
3613
3614        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3615        let filtered = filter_blocked_fields(value, &blocked);
3616
3617        assert_eq!(filtered["user"]["id"], 1);
3618        assert_eq!(filtered["user"]["profile"]["name"], "Alice");
3619        assert!(filtered["user"]["profile"].get("password").is_none());
3620    }
3621
3622    #[test]
3623    fn test_filter_blocked_fields_in_array() {
3624        let value = serde_json::json!([
3625            { "id": 1, "name": "Alice", "password": "secret1" },
3626            { "id": 2, "name": "Bob", "password": "secret2" }
3627        ]);
3628
3629        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3630        let filtered = filter_blocked_fields(value, &blocked);
3631
3632        let arr = filtered.as_array().unwrap();
3633        assert_eq!(arr.len(), 2);
3634        assert_eq!(arr[0]["id"], 1);
3635        assert_eq!(arr[0]["name"], "Alice");
3636        assert!(arr[0].get("password").is_none());
3637        assert_eq!(arr[1]["id"], 2);
3638        assert_eq!(arr[1]["name"], "Bob");
3639        assert!(arr[1].get("password").is_none());
3640    }
3641
3642    #[test]
3643    fn test_filter_blocked_fields_empty_blocklist() {
3644        let value = serde_json::json!({
3645            "id": 1,
3646            "password": "secret123"
3647        });
3648
3649        let blocked: HashSet<String> = HashSet::new();
3650        let filtered = filter_blocked_fields(value.clone(), &blocked);
3651
3652        // Should be unchanged
3653        assert_eq!(filtered, value);
3654    }
3655
3656    #[test]
3657    fn test_filter_blocked_fields_primitive_values() {
3658        // Primitives should pass through unchanged
3659        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3660
3661        assert_eq!(
3662            filter_blocked_fields(JsonValue::String("test".into()), &blocked),
3663            JsonValue::String("test".into())
3664        );
3665        assert_eq!(
3666            filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
3667            JsonValue::Number(42.into())
3668        );
3669        assert_eq!(
3670            filter_blocked_fields(JsonValue::Bool(true), &blocked),
3671            JsonValue::Bool(true)
3672        );
3673        assert_eq!(
3674            filter_blocked_fields(JsonValue::Null, &blocked),
3675            JsonValue::Null
3676        );
3677    }
3678
3679    #[tokio::test]
3680    async fn test_execute_with_blocked_fields() {
3681        let code = r#"
3682            const user = await api.get('/users/1');
3683            return user;
3684        "#;
3685
3686        let mut compiler = PlanCompiler::new();
3687        let plan = compiler.compile_code(code).expect("Should compile");
3688
3689        let mut mock_http = MockHttpExecutor::new();
3690        mock_http.add_response(
3691            "/users/1",
3692            serde_json::json!({
3693                "id": 1,
3694                "name": "Alice",
3695                "password": "secret123",
3696                "apiKey": "key-abc"
3697            }),
3698        );
3699
3700        // Create config with blocked fields
3701        let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
3702
3703        let mut executor = PlanExecutor::new(mock_http, config);
3704        let result = executor.execute(&plan).await.expect("Should execute");
3705
3706        // Blocked fields should be filtered out
3707        assert_eq!(result.value["id"], 1);
3708        assert_eq!(result.value["name"], "Alice");
3709        assert!(result.value.get("password").is_none());
3710        assert!(result.value.get("apiKey").is_none());
3711    }
3712
3713    #[tokio::test]
3714    async fn test_execute_nested_blocked_fields() {
3715        let code = r#"
3716            const data = await api.get('/data');
3717            return data;
3718        "#;
3719
3720        let mut compiler = PlanCompiler::new();
3721        let plan = compiler.compile_code(code).expect("Should compile");
3722
3723        let mut mock_http = MockHttpExecutor::new();
3724        mock_http.add_response(
3725            "/data",
3726            serde_json::json!({
3727                "users": [
3728                    { "id": 1, "name": "Alice", "secret": "hidden1" },
3729                    { "id": 2, "name": "Bob", "secret": "hidden2" }
3730                ],
3731                "config": {
3732                    "setting": "value",
3733                    "secret": "also-hidden"
3734                }
3735            }),
3736        );
3737
3738        // Create config with blocked fields
3739        let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
3740
3741        let mut executor = PlanExecutor::new(mock_http, config);
3742        let result = executor.execute(&plan).await.expect("Should execute");
3743
3744        // Secret should be filtered from all nested locations
3745        let users = result.value["users"].as_array().unwrap();
3746        assert_eq!(users[0]["name"], "Alice");
3747        assert!(users[0].get("secret").is_none());
3748        assert_eq!(users[1]["name"], "Bob");
3749        assert!(users[1].get("secret").is_none());
3750
3751        assert_eq!(result.value["config"]["setting"], "value");
3752        assert!(result.value["config"].get("secret").is_none());
3753    }
3754
3755    // =========================================================================
3756    // Output Validation Tests
3757    // =========================================================================
3758
3759    #[test]
3760    fn test_find_blocked_fields_in_output_simple() {
3761        let value = serde_json::json!({
3762            "id": 1,
3763            "name": "Alice",
3764            "ssn": "123-45-6789"
3765        });
3766
3767        let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3768        let violations = find_blocked_fields_in_output(&value, &blocked);
3769
3770        assert_eq!(violations.len(), 1);
3771        assert_eq!(violations[0], "ssn");
3772    }
3773
3774    #[test]
3775    fn test_find_blocked_fields_in_output_nested() {
3776        let value = serde_json::json!({
3777            "user": {
3778                "profile": {
3779                    "name": "Alice",
3780                    "salary": 100000
3781                }
3782            }
3783        });
3784
3785        let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
3786        let violations = find_blocked_fields_in_output(&value, &blocked);
3787
3788        assert_eq!(violations.len(), 1);
3789        assert!(violations[0].contains("salary"));
3790    }
3791
3792    #[test]
3793    fn test_find_blocked_fields_in_output_array() {
3794        let value = serde_json::json!([
3795            { "id": 1, "ssn": "111" },
3796            { "id": 2, "ssn": "222" }
3797        ]);
3798
3799        let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3800        let violations = find_blocked_fields_in_output(&value, &blocked);
3801
3802        // Should find ssn in both array elements
3803        assert_eq!(violations.len(), 2);
3804    }
3805
3806    #[test]
3807    fn test_find_blocked_fields_in_output_empty_blocklist() {
3808        let value = serde_json::json!({
3809            "id": 1,
3810            "ssn": "123-45-6789"
3811        });
3812
3813        let blocked: HashSet<String> = HashSet::new();
3814        let violations = find_blocked_fields_in_output(&value, &blocked);
3815
3816        assert!(violations.is_empty());
3817    }
3818
3819    #[test]
3820    fn test_find_blocked_fields_in_output_no_violations() {
3821        let value = serde_json::json!({
3822            "id": 1,
3823            "name": "Alice"
3824        });
3825
3826        let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
3827        let violations = find_blocked_fields_in_output(&value, &blocked);
3828
3829        assert!(violations.is_empty());
3830    }
3831
3832    #[tokio::test]
3833    async fn test_execute_output_blocked_fields_rejected() {
3834        let code = r#"
3835            const user = await api.get('/users/1');
3836            return { name: user.name, ssn: user.ssn };
3837        "#;
3838
3839        let mut compiler = PlanCompiler::new();
3840        let plan = compiler.compile_code(code).expect("Should compile");
3841
3842        let mut mock_http = MockHttpExecutor::new();
3843        mock_http.add_response(
3844            "/users/1",
3845            serde_json::json!({
3846                "id": 1,
3847                "name": "Alice",
3848                "ssn": "123-45-6789"
3849            }),
3850        );
3851
3852        // Note: internal blocklist is empty, so ssn gets through to the script
3853        // But output blocklist should catch it in the return value
3854        let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3855
3856        let mut executor = PlanExecutor::new(mock_http, config);
3857        let result = executor.execute(&plan).await;
3858
3859        // Should fail because output contains blocked field
3860        assert!(result.is_err());
3861        let err = result.unwrap_err();
3862        assert!(format!("{:?}", err).contains("ssn"));
3863    }
3864
3865    #[tokio::test]
3866    async fn test_execute_output_blocked_fields_internal_use_allowed() {
3867        // Script reads user data but only returns safe fields - should succeed
3868        let code = r#"
3869            const user = await api.get('/users/1');
3870            return { id: user.id, name: user.name };
3871        "#;
3872
3873        let mut compiler = PlanCompiler::new();
3874        let plan = compiler.compile_code(code).expect("Should compile");
3875
3876        let mut mock_http = MockHttpExecutor::new();
3877        mock_http.add_response(
3878            "/users/1",
3879            serde_json::json!({
3880                "id": 1,
3881                "name": "Alice",
3882                "ssn": "123-45-6789"
3883            }),
3884        );
3885
3886        // Output blocklist - ssn can be read but not returned
3887        // Note: This doesn't prevent script from accessing ssn, just returning it
3888        let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3889
3890        let mut executor = PlanExecutor::new(mock_http, config);
3891        let result = executor.execute(&plan).await.expect("Should succeed");
3892
3893        // Script read user data but only returned safe fields
3894        assert_eq!(result.value["id"], 1);
3895        assert_eq!(result.value["name"], "Alice");
3896        assert!(result.value.get("ssn").is_none());
3897    }
3898
3899    #[tokio::test]
3900    async fn test_execute_both_blocklists() {
3901        // Test that internal blocklist AND output blocklist work together
3902        let code = r#"
3903            const user = await api.get('/users/1');
3904            return { name: user.name, dateOfBirth: user.dateOfBirth };
3905        "#;
3906
3907        let mut compiler = PlanCompiler::new();
3908        let plan = compiler.compile_code(code).expect("Should compile");
3909
3910        let mut mock_http = MockHttpExecutor::new();
3911        mock_http.add_response(
3912            "/users/1",
3913            serde_json::json!({
3914                "id": 1,
3915                "name": "Alice",
3916                "password": "secret123",
3917                "dateOfBirth": "1990-01-01"
3918            }),
3919        );
3920
3921        // Internal blocklist: password is stripped from API response
3922        // Output blocklist: dateOfBirth can be used but not returned
3923        let config = ExecutionConfig::default()
3924            .with_blocked_fields(["password"])
3925            .with_output_blocked_fields(["dateOfBirth"]);
3926
3927        let mut executor = PlanExecutor::new(mock_http, config);
3928        let result = executor.execute(&plan).await;
3929
3930        // Should fail because output contains dateOfBirth
3931        assert!(result.is_err());
3932        let err = result.unwrap_err();
3933        assert!(format!("{:?}", err).contains("dateOfBirth"));
3934    }
3935
3936    // ========================================================================
3937    // Tests for pre-bound variables (args) and conditionals
3938    // ========================================================================
3939
3940    #[tokio::test]
3941    async fn test_prebound_args_comparison() {
3942        // Test that `if (args.k > args.n)` works with pre-bound args
3943        let code = r#"
3944            if (args.k > args.n) {
3945                return { error: 'k must be <= n' };
3946            }
3947            return { ok: true };
3948        "#;
3949
3950        let mut compiler = PlanCompiler::new();
3951        let plan = compiler.compile_code(code).expect("Should compile");
3952
3953        let mock_http = MockHttpExecutor::new();
3954        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3955        executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
3956
3957        let result = executor.execute(&plan).await.expect("Should execute");
3958        assert_eq!(
3959            result.value["error"], "k must be <= n",
3960            "Expected error for k > n, got: {:?}",
3961            result.value
3962        );
3963    }
3964
3965    #[tokio::test]
3966    async fn test_prebound_args_strict_equality() {
3967        // Test that `args.k === 0` works
3968        let code = r#"
3969            if (args.k === 0) {
3970                return { result: 1 };
3971            }
3972            return { result: 'not zero' };
3973        "#;
3974
3975        let mut compiler = PlanCompiler::new();
3976        let plan = compiler.compile_code(code).expect("Should compile");
3977
3978        let mock_http = MockHttpExecutor::new();
3979        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3980        executor.set_variable("args", serde_json::json!({"k": 0}));
3981
3982        let result = executor.execute(&plan).await.expect("Should execute");
3983        assert_eq!(result.value["result"], 1);
3984    }
3985
3986    #[tokio::test]
3987    async fn test_assignment_expression_in_statement() {
3988        // Test that `k = newValue` works as a statement (Expr::Assign)
3989        let code = r#"
3990            let k = 5;
3991            k = 2;
3992            return { k: k };
3993        "#;
3994
3995        let mut compiler = PlanCompiler::new();
3996        let plan = compiler.compile_code(code).expect("Should compile");
3997
3998        let mock_http = MockHttpExecutor::new();
3999        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4000
4001        let result = executor.execute(&plan).await.expect("Should execute");
4002        assert_eq!(result.value["k"], 2);
4003    }
4004
4005    #[tokio::test]
4006    async fn test_assignment_swap_variables() {
4007        // Test swapping two let-bound variables
4008        let code = r#"
4009            let a = 3;
4010            let b = 7;
4011            if (a < b) {
4012                const old_a = a;
4013                a = b;
4014                b = old_a;
4015            }
4016            return { a: a, b: b };
4017        "#;
4018
4019        let mut compiler = PlanCompiler::new();
4020        let plan = compiler.compile_code(code).expect("Should compile");
4021
4022        let mock_http = MockHttpExecutor::new();
4023        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4024
4025        let result = executor.execute(&plan).await.expect("Should execute");
4026        assert_eq!(result.value["a"], 7);
4027        assert_eq!(result.value["b"], 3);
4028    }
4029
4030    // ========================================================================
4031    // MCP call tests (require mcp-code-mode feature)
4032    // ========================================================================
4033
4034    #[cfg(feature = "mcp-code-mode")]
4035    mod mcp_tests {
4036        use super::*;
4037
4038        /// Mock MCP executor that simulates a calculator server.
4039        struct MockCalculatorExecutor;
4040
4041        #[async_trait::async_trait]
4042        impl McpExecutor for MockCalculatorExecutor {
4043            async fn call_tool(
4044                &self,
4045                _server_id: &str,
4046                tool_name: &str,
4047                args: JsonValue,
4048            ) -> Result<JsonValue, ExecutionError> {
4049                match tool_name {
4050                    "add" => {
4051                        let a = args["a"].as_f64().unwrap_or(0.0);
4052                        let b = args["b"].as_f64().unwrap_or(0.0);
4053                        Ok(serde_json::json!({"result": a + b}))
4054                    },
4055                    "subtract" => {
4056                        let a = args["a"].as_f64().unwrap_or(0.0);
4057                        let b = args["b"].as_f64().unwrap_or(0.0);
4058                        Ok(serde_json::json!({"result": a - b}))
4059                    },
4060                    "multiply" => {
4061                        let a = args["a"].as_f64().unwrap_or(0.0);
4062                        let b = args["b"].as_f64().unwrap_or(0.0);
4063                        Ok(serde_json::json!({"result": a * b}))
4064                    },
4065                    "divide" => {
4066                        let a = args["a"].as_f64().unwrap_or(0.0);
4067                        let b = args["b"].as_f64().unwrap_or(1.0);
4068                        Ok(serde_json::json!({"result": a / b}))
4069                    },
4070                    "power" => {
4071                        let base = args["base"].as_f64().unwrap_or(0.0);
4072                        let exponent = args["exponent"].as_f64().unwrap_or(1.0);
4073                        Ok(serde_json::json!({"result": base.powf(exponent)}))
4074                    },
4075                    "sqrt" => {
4076                        let n = args["n"].as_f64().unwrap_or(0.0);
4077                        Ok(serde_json::json!({"result": n.sqrt()}))
4078                    },
4079                    _ => Err(ExecutionError::RuntimeError {
4080                        message: format!("Unknown tool: {}", tool_name),
4081                    }),
4082                }
4083            }
4084        }
4085
4086        #[tokio::test]
4087        async fn test_mcp_call_simple() {
4088            let code = r#"
4089                const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
4090                return result;
4091            "#;
4092
4093            let mut compiler = PlanCompiler::new();
4094            let plan = compiler.compile_code(code).expect("Should compile");
4095
4096            let mock_http = MockHttpExecutor::new();
4097            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4098            executor.set_mcp_executor(MockCalculatorExecutor);
4099
4100            let result = executor.execute(&plan).await.expect("Should execute");
4101            assert_eq!(result.value["result"], 8.0);
4102        }
4103
4104        #[tokio::test]
4105        async fn test_mcp_call_with_args() {
4106            // Test mcp.call using pre-bound args variable
4107            let code = r#"
4108                const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
4109                return { sum: result.result };
4110            "#;
4111
4112            let mut compiler = PlanCompiler::new();
4113            let plan = compiler.compile_code(code).expect("Should compile");
4114
4115            let mock_http = MockHttpExecutor::new();
4116            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4117            executor.set_mcp_executor(MockCalculatorExecutor);
4118            executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
4119
4120            let result = executor.execute(&plan).await.expect("Should execute");
4121            assert_eq!(result.value["sum"], 30.0);
4122        }
4123
4124        #[tokio::test]
4125        async fn test_mcp_assignment_in_loop() {
4126            // Test `result = await mcp.call(...)` assignment inside a loop
4127            let code = r#"
4128                let result = { result: 1 };
4129                for (const i of [2, 3, 4, 5]) {
4130                    const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
4131                    result = mul;
4132                }
4133                return { factorial: result.result };
4134            "#;
4135
4136            let mut compiler = PlanCompiler::new();
4137            let plan = compiler.compile_code(code).expect("Should compile");
4138
4139            let mock_http = MockHttpExecutor::new();
4140            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4141            executor.set_mcp_executor(MockCalculatorExecutor);
4142
4143            let result = executor.execute(&plan).await.expect("Should execute");
4144            // 1 * 2 * 3 * 4 * 5 = 120
4145            assert_eq!(result.value["factorial"], 120.0);
4146        }
4147
4148        #[tokio::test]
4149        async fn test_combinations_c_5_3() {
4150            // Full combinations script: C(5,3) = 10
4151            let code = r#"
4152if (args.k > args.n) {
4153  return { error: 'k must be <= n', n: args.n, k: args.k };
4154}
4155if (args.k === 0 || args.k === args.n) {
4156  return { n: args.n, k: args.k, result: 1 };
4157}
4158let k = args.k;
4159const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4160let nmk = complement.result;
4161if (nmk < k) {
4162  const old_k = k;
4163  k = nmk;
4164  nmk = old_k;
4165}
4166let result = { result: 1 };
4167for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4168  if (i > k) { break; }
4169  const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4170  const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4171  result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4172}
4173return { n: args.n, k: args.k, result: result.result };
4174            "#;
4175
4176            let mut compiler = PlanCompiler::new();
4177            let plan = compiler.compile_code(code).expect("Should compile");
4178
4179            let mock_http = MockHttpExecutor::new();
4180            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4181            executor.set_mcp_executor(MockCalculatorExecutor);
4182            executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
4183
4184            let result = executor.execute(&plan).await.expect("Should execute");
4185            assert_eq!(
4186                result.value["result"], 10.0,
4187                "C(5,3) should be 10, got: {:?}",
4188                result.value
4189            );
4190        }
4191
4192        #[tokio::test]
4193        async fn test_combinations_k_greater_than_n() {
4194            // C(3,5) should return error
4195            let code = r#"
4196if (args.k > args.n) {
4197  return { error: 'k must be <= n', n: args.n, k: args.k };
4198}
4199return { result: 'should not reach here' };
4200            "#;
4201
4202            let mut compiler = PlanCompiler::new();
4203            let plan = compiler.compile_code(code).expect("Should compile");
4204
4205            let mock_http = MockHttpExecutor::new();
4206            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4207            executor.set_mcp_executor(MockCalculatorExecutor);
4208            executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4209
4210            let result = executor.execute(&plan).await.expect("Should execute");
4211            assert_eq!(
4212                result.value["error"], "k must be <= n",
4213                "C(3,5) should return error, got: {:?}",
4214                result.value
4215            );
4216        }
4217
4218        #[tokio::test]
4219        async fn test_combinations_c_5_2() {
4220            // C(5,2) = 10 — no swap needed
4221            let code = r#"
4222if (args.k > args.n) {
4223  return { error: 'k must be <= n', n: args.n, k: args.k };
4224}
4225if (args.k === 0 || args.k === args.n) {
4226  return { n: args.n, k: args.k, result: 1 };
4227}
4228let k = args.k;
4229const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4230let nmk = complement.result;
4231if (nmk < k) {
4232  const old_k = k;
4233  k = nmk;
4234  nmk = old_k;
4235}
4236let result = { result: 1 };
4237for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4238  if (i > k) { break; }
4239  const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4240  const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4241  result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4242}
4243return { n: args.n, k: args.k, result: result.result };
4244            "#;
4245
4246            let mut compiler = PlanCompiler::new();
4247            let plan = compiler.compile_code(code).expect("Should compile");
4248
4249            let mock_http = MockHttpExecutor::new();
4250            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4251            executor.set_mcp_executor(MockCalculatorExecutor);
4252            executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
4253
4254            let result = executor.execute(&plan).await.expect("Should execute");
4255            assert_eq!(
4256                result.value["result"], 10.0,
4257                "C(5,2) should be 10, got: {:?}",
4258                result.value
4259            );
4260        }
4261
4262        #[tokio::test]
4263        async fn test_combinations_edge_cases() {
4264            let code = r#"
4265if (args.k === 0 || args.k === args.n) {
4266  return { result: 1 };
4267}
4268return { result: 'not edge case' };
4269            "#;
4270
4271            let mut compiler = PlanCompiler::new();
4272            let plan = compiler.compile_code(code).expect("Should compile");
4273
4274            // Test k=0
4275            let mock_http = MockHttpExecutor::new();
4276            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4277            executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
4278            let result = executor.execute(&plan).await.expect("Should execute");
4279            assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
4280
4281            // Test k=n
4282            let mock_http = MockHttpExecutor::new();
4283            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4284            executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
4285            let result = executor.execute(&plan).await.expect("Should execute");
4286            assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
4287        }
4288
4289        #[tokio::test]
4290        async fn test_solve_quadratic() {
4291            // x² - 3x + 2 = 0 → roots [2, 1]
4292            let code = r#"
4293const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
4294const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
4295const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
4296const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
4297const root_type = discriminant.result > 0 ? 'two_real'
4298  : discriminant.result === 0 ? 'one_real' : 'complex';
4299if (discriminant.result < 0) {
4300  return { discriminant: discriminant.result, root_type: root_type, roots: [] };
4301}
4302const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
4303const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
4304const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
4305const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
4306const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
4307const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
4308const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
4309return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
4310            "#;
4311
4312            let mut compiler = PlanCompiler::new();
4313            let plan = compiler.compile_code(code).expect("Should compile");
4314
4315            let mock_http = MockHttpExecutor::new();
4316            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4317            executor.set_mcp_executor(MockCalculatorExecutor);
4318            executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
4319
4320            let result = executor.execute(&plan).await.expect("Should execute");
4321            assert_eq!(result.value["root_type"], "two_real");
4322            assert_eq!(result.value["discriminant"], 1.0);
4323            let roots = result.value["roots"]
4324                .as_array()
4325                .expect("roots should be array");
4326            assert_eq!(roots.len(), 2);
4327            assert_eq!(roots[0], 2.0);
4328            assert_eq!(roots[1], 1.0);
4329        }
4330    }
4331
4332    // =========================================================================
4333    // String method integration tests (compile + execute)
4334    // =========================================================================
4335
4336    #[tokio::test]
4337    async fn test_string_includes() {
4338        let code = r#"
4339            const text = "hello world";
4340            return { found: text.includes("world"), miss: text.includes("xyz") };
4341        "#;
4342
4343        let mut compiler = PlanCompiler::new();
4344        let plan = compiler.compile_code(code).expect("Should compile");
4345
4346        let mock_http = MockHttpExecutor::new();
4347        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4348        let result = executor.execute(&plan).await.expect("Should execute");
4349
4350        assert_eq!(result.value["found"], true);
4351        assert_eq!(result.value["miss"], false);
4352    }
4353
4354    #[tokio::test]
4355    async fn test_string_index_of() {
4356        let code = r#"
4357            const text = "abcdef";
4358            return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
4359        "#;
4360
4361        let mut compiler = PlanCompiler::new();
4362        let plan = compiler.compile_code(code).expect("Should compile");
4363
4364        let mock_http = MockHttpExecutor::new();
4365        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4366        let result = executor.execute(&plan).await.expect("Should execute");
4367
4368        assert_eq!(result.value["idx"], 2);
4369        assert_eq!(result.value["miss"], -1);
4370    }
4371
4372    #[tokio::test]
4373    async fn test_string_length() {
4374        let code = r#"
4375            const text = "hello";
4376            return { len: text.length };
4377        "#;
4378
4379        let mut compiler = PlanCompiler::new();
4380        let plan = compiler.compile_code(code).expect("Should compile");
4381
4382        let mock_http = MockHttpExecutor::new();
4383        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4384        let result = executor.execute(&plan).await.expect("Should execute");
4385
4386        assert_eq!(result.value["len"], 5);
4387    }
4388
4389    #[tokio::test]
4390    async fn test_string_slice() {
4391        let code = r#"
4392            const text = "hello world";
4393            return { first: text.slice(0, 5), rest: text.slice(6, 11) };
4394        "#;
4395
4396        let mut compiler = PlanCompiler::new();
4397        let plan = compiler.compile_code(code).expect("Should compile");
4398
4399        let mock_http = MockHttpExecutor::new();
4400        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4401        let result = executor.execute(&plan).await.expect("Should execute");
4402
4403        assert_eq!(result.value["first"], "hello");
4404        assert_eq!(result.value["rest"], "world");
4405    }
4406
4407    #[tokio::test]
4408    async fn test_string_concat() {
4409        let code = r#"
4410            const greeting = "hello";
4411            return { result: greeting.concat(" world") };
4412        "#;
4413
4414        let mut compiler = PlanCompiler::new();
4415        let plan = compiler.compile_code(code).expect("Should compile");
4416
4417        let mock_http = MockHttpExecutor::new();
4418        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4419        let result = executor.execute(&plan).await.expect("Should execute");
4420
4421        assert_eq!(result.value["result"], "hello world");
4422    }
4423
4424    #[tokio::test]
4425    async fn test_string_includes_in_filter() {
4426        // Real-world pattern: filter array items by string content
4427        let code = r#"
4428            const items = [
4429                { name: "TIMESTAMP_2024", desc: "A timestamped record" },
4430                { name: "PERSON_1", desc: "A person entity" },
4431                { name: "TIMESTAMP_2025", desc: "Another timestamped record" }
4432            ];
4433            const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
4434            return { count: timestamped.length, names: timestamped.map(t => t.name) };
4435        "#;
4436
4437        let mut compiler = PlanCompiler::new();
4438        let plan = compiler.compile_code(code).expect("Should compile");
4439
4440        let mock_http = MockHttpExecutor::new();
4441        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4442        let result = executor.execute(&plan).await.expect("Should execute");
4443
4444        assert_eq!(result.value["count"], 2);
4445        let names = result.value["names"].as_array().unwrap();
4446        assert_eq!(names[0], "TIMESTAMP_2024");
4447        assert_eq!(names[1], "TIMESTAMP_2025");
4448    }
4449
4450    #[tokio::test]
4451    async fn test_array_includes_still_works() {
4452        // Regression: array .includes() must still work
4453        let code = r#"
4454            const ids = ["alice", "bob", "charlie"];
4455            return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
4456        "#;
4457
4458        let mut compiler = PlanCompiler::new();
4459        let plan = compiler.compile_code(code).expect("Should compile");
4460
4461        let mock_http = MockHttpExecutor::new();
4462        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4463        let result = executor.execute(&plan).await.expect("Should execute");
4464
4465        assert_eq!(result.value["has_bob"], true);
4466        assert_eq!(result.value["has_dave"], false);
4467    }
4468
4469    // =========================================================================
4470    // Built-in function compilation tests
4471    // =========================================================================
4472
4473    #[test]
4474    fn test_compile_parse_float() {
4475        let code = r#"
4476            const x = parseFloat("3.14");
4477            return x;
4478        "#;
4479        let mut compiler = PlanCompiler::new();
4480        let plan = compiler
4481            .compile_code(code)
4482            .expect("parseFloat should compile");
4483        assert_eq!(plan.steps.len(), 2); // Assign + Return
4484    }
4485
4486    #[test]
4487    fn test_compile_parse_int() {
4488        let code = r#"
4489            const x = parseInt("42");
4490            return x;
4491        "#;
4492        let mut compiler = PlanCompiler::new();
4493        compiler
4494            .compile_code(code)
4495            .expect("parseInt should compile");
4496    }
4497
4498    #[test]
4499    fn test_compile_math_abs() {
4500        let code = r#"
4501            const x = Math.abs(-5);
4502            return x;
4503        "#;
4504        let mut compiler = PlanCompiler::new();
4505        compiler
4506            .compile_code(code)
4507            .expect("Math.abs should compile");
4508    }
4509
4510    #[test]
4511    fn test_compile_math_max() {
4512        let code = r#"
4513            const x = Math.max(1, 2, 3);
4514            return x;
4515        "#;
4516        let mut compiler = PlanCompiler::new();
4517        compiler
4518            .compile_code(code)
4519            .expect("Math.max should compile");
4520    }
4521
4522    #[test]
4523    fn test_compile_object_keys() {
4524        let code = r#"
4525            const obj = { a: 1, b: 2 };
4526            const keys = Object.keys(obj);
4527            return keys;
4528        "#;
4529        let mut compiler = PlanCompiler::new();
4530        compiler
4531            .compile_code(code)
4532            .expect("Object.keys should compile");
4533    }
4534
4535    #[test]
4536    fn test_compile_object_entries() {
4537        let code = r#"
4538            const obj = { x: 10 };
4539            const entries = Object.entries(obj);
4540            return entries;
4541        "#;
4542        let mut compiler = PlanCompiler::new();
4543        compiler
4544            .compile_code(code)
4545            .expect("Object.entries should compile");
4546    }
4547
4548    #[test]
4549    fn test_compile_unary_plus() {
4550        let code = r#"
4551            const x = +"42";
4552            return x;
4553        "#;
4554        let mut compiler = PlanCompiler::new();
4555        compiler.compile_code(code).expect("unary + should compile");
4556    }
4557
4558    #[test]
4559    fn test_compile_sort_with_comparator() {
4560        let code = r#"
4561            const arr = [3, 1, 2];
4562            const sorted = arr.sort((a, b) => a - b);
4563            return sorted;
4564        "#;
4565        let mut compiler = PlanCompiler::new();
4566        compiler
4567            .compile_code(code)
4568            .expect("sort with comparator should compile");
4569    }
4570
4571    #[test]
4572    fn test_compile_sort_without_comparator() {
4573        let code = r#"
4574            const arr = ["b", "a", "c"];
4575            const sorted = arr.sort();
4576            return sorted;
4577        "#;
4578        let mut compiler = PlanCompiler::new();
4579        compiler
4580            .compile_code(code)
4581            .expect("sort without comparator should compile");
4582    }
4583
4584    // =========================================================================
4585    // End-to-end execution tests for new features
4586    // =========================================================================
4587
4588    #[tokio::test]
4589    async fn test_execute_parse_float() {
4590        let code = r#"
4591            const x = parseFloat("3.14");
4592            return x;
4593        "#;
4594        let mut compiler = PlanCompiler::new();
4595        let plan = compiler.compile_code(code).unwrap();
4596        let mock_http = MockHttpExecutor::new();
4597        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4598        let result = executor.execute(&plan).await.unwrap();
4599        // Why: test fixture uses 3.14 as a representative non-integer parse target,
4600        // not the mathematical PI constant — clippy::approx_constant is a false positive here.
4601        #[allow(clippy::approx_constant)]
4602        let expected = serde_json::json!(3.14);
4603        assert_eq!(result.value, expected);
4604    }
4605
4606    #[tokio::test]
4607    async fn test_execute_math_abs_and_sort() {
4608        let code = r#"
4609            const items = [
4610                { name: "a", val: -5 },
4611                { name: "b", val: 3 },
4612                { name: "c", val: -1 }
4613            ];
4614            const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
4615            return sorted.map(x => x.name);
4616        "#;
4617        let mut compiler = PlanCompiler::new();
4618        let plan = compiler.compile_code(code).unwrap();
4619        let mock_http = MockHttpExecutor::new();
4620        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4621        let result = executor.execute(&plan).await.unwrap();
4622        assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
4623    }
4624
4625    #[tokio::test]
4626    async fn test_execute_object_keys() {
4627        let code = r#"
4628            const obj = { x: 1, y: 2, z: 3 };
4629            return Object.keys(obj).length;
4630        "#;
4631        let mut compiler = PlanCompiler::new();
4632        let plan = compiler.compile_code(code).unwrap();
4633        let mock_http = MockHttpExecutor::new();
4634        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4635        let result = executor.execute(&plan).await.unwrap();
4636        assert_eq!(result.value, serde_json::json!(3));
4637    }
4638
4639    #[tokio::test]
4640    async fn test_execute_unary_plus() {
4641        let code = r#"
4642            const x = +"42";
4643            return x;
4644        "#;
4645        let mut compiler = PlanCompiler::new();
4646        let plan = compiler.compile_code(code).unwrap();
4647        let mock_http = MockHttpExecutor::new();
4648        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4649        let result = executor.execute(&plan).await.unwrap();
4650        assert_eq!(result.value, serde_json::json!(42.0));
4651    }
4652
4653    #[tokio::test]
4654    async fn test_execute_number_cast() {
4655        let code = r#"
4656            const x = Number("99.5");
4657            return x;
4658        "#;
4659        let mut compiler = PlanCompiler::new();
4660        let plan = compiler.compile_code(code).unwrap();
4661        let mock_http = MockHttpExecutor::new();
4662        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4663        let result = executor.execute(&plan).await.unwrap();
4664        assert_eq!(result.value, serde_json::json!(99.5));
4665    }
4666
4667    #[tokio::test]
4668    async fn test_execute_math_round_floor_ceil() {
4669        let code = r#"
4670            return {
4671                round: Math.round(3.7),
4672                floor: Math.floor(3.7),
4673                ceil: Math.ceil(3.2)
4674            };
4675        "#;
4676        let mut compiler = PlanCompiler::new();
4677        let plan = compiler.compile_code(code).unwrap();
4678        let mock_http = MockHttpExecutor::new();
4679        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4680        let result = executor.execute(&plan).await.unwrap();
4681        assert_eq!(result.value["round"], serde_json::json!(4.0));
4682        assert_eq!(result.value["floor"], serde_json::json!(3.0));
4683        assert_eq!(result.value["ceil"], serde_json::json!(4.0));
4684    }
4685
4686    // =========================================================================
4687    // Object Spread Tests
4688    // =========================================================================
4689
4690    #[test]
4691    fn test_compile_object_spread_basic() {
4692        let code = r#"
4693            const base = { id: 1, name: "Alice" };
4694            const extended = { ...base, age: 30 };
4695            return extended;
4696        "#;
4697        let mut compiler = PlanCompiler::new();
4698        let plan = compiler
4699            .compile_code(code)
4700            .expect("Object spread should compile");
4701        assert!(plan.steps.len() >= 2);
4702    }
4703
4704    #[tokio::test]
4705    async fn test_execute_object_spread_basic() {
4706        let code = r#"
4707            const base = { id: 1, name: "Alice" };
4708            const extended = { ...base, age: 30 };
4709            return extended;
4710        "#;
4711        let mut compiler = PlanCompiler::new();
4712        let plan = compiler.compile_code(code).unwrap();
4713        let mock_http = MockHttpExecutor::new();
4714        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4715        let result = executor.execute(&plan).await.unwrap();
4716        assert_eq!(result.value["id"], serde_json::json!(1));
4717        assert_eq!(result.value["name"], serde_json::json!("Alice"));
4718        assert_eq!(result.value["age"], serde_json::json!(30));
4719    }
4720
4721    #[tokio::test]
4722    async fn test_execute_object_spread_override() {
4723        // Later properties should override spread properties (JS semantics)
4724        let code = r#"
4725            const obj = { id: 1, name: "old" };
4726            const updated = { ...obj, name: "new" };
4727            return updated;
4728        "#;
4729        let mut compiler = PlanCompiler::new();
4730        let plan = compiler.compile_code(code).unwrap();
4731        let mock_http = MockHttpExecutor::new();
4732        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4733        let result = executor.execute(&plan).await.unwrap();
4734        assert_eq!(result.value["id"], serde_json::json!(1));
4735        assert_eq!(result.value["name"], serde_json::json!("new"));
4736    }
4737
4738    #[tokio::test]
4739    async fn test_execute_object_spread_multiple() {
4740        let code = r#"
4741            const a = { x: 1 };
4742            const b = { y: 2 };
4743            const merged = { ...a, ...b, z: 3 };
4744            return merged;
4745        "#;
4746        let mut compiler = PlanCompiler::new();
4747        let plan = compiler.compile_code(code).unwrap();
4748        let mock_http = MockHttpExecutor::new();
4749        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4750        let result = executor.execute(&plan).await.unwrap();
4751        assert_eq!(result.value["x"], serde_json::json!(1));
4752        assert_eq!(result.value["y"], serde_json::json!(2));
4753        assert_eq!(result.value["z"], serde_json::json!(3));
4754    }
4755
4756    #[tokio::test]
4757    async fn test_execute_object_spread_with_api_result() {
4758        // Primary use case: spread API result into a new object
4759        let code = r#"
4760            const config = await api.get('/config');
4761            const result = { ...config, extra: "added" };
4762            return result;
4763        "#;
4764        let mut compiler = PlanCompiler::new();
4765        let plan = compiler.compile_code(code).unwrap();
4766        let mut mock_http = MockHttpExecutor::new();
4767        mock_http.add_response(
4768            "/config",
4769            serde_json::json!({ "key": "value", "enabled": true }),
4770        );
4771        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4772        let result = executor.execute(&plan).await.unwrap();
4773        assert_eq!(result.value["key"], serde_json::json!("value"));
4774        assert_eq!(result.value["enabled"], serde_json::json!(true));
4775        assert_eq!(result.value["extra"], serde_json::json!("added"));
4776    }
4777
4778    #[tokio::test]
4779    async fn test_execute_object_spread_non_object_noop() {
4780        // Spreading a non-object should be a no-op (matches JS behavior)
4781        let code = r#"
4782            const x = 42;
4783            const obj = { ...x, name: "test" };
4784            return obj;
4785        "#;
4786        let mut compiler = PlanCompiler::new();
4787        let plan = compiler.compile_code(code).unwrap();
4788        let mock_http = MockHttpExecutor::new();
4789        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4790        let result = executor.execute(&plan).await.unwrap();
4791        assert_eq!(result.value["name"], serde_json::json!("test"));
4792        // x (number) should not add any properties
4793        assert!(result.value.as_object().unwrap().len() == 1);
4794    }
4795
4796    #[tokio::test]
4797    async fn test_execute_object_spread_preserves_order() {
4798        // Spread before explicit property: explicit wins
4799        // Explicit before spread: spread wins
4800        let code = r#"
4801            const obj = { a: 1, b: 2 };
4802            const result = { b: 99, ...obj, a: 100 };
4803            return result;
4804        "#;
4805        let mut compiler = PlanCompiler::new();
4806        let plan = compiler.compile_code(code).unwrap();
4807        let mock_http = MockHttpExecutor::new();
4808        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4809        let result = executor.execute(&plan).await.unwrap();
4810        // { b: 99 } then { ...obj } → b overridden to 2, then { a: 100 } → a overridden to 100
4811        assert_eq!(result.value["a"], serde_json::json!(100));
4812        assert_eq!(result.value["b"], serde_json::json!(2));
4813    }
4814
4815    // =========================================================================
4816    // Object Destructuring Tests
4817    // =========================================================================
4818
4819    #[test]
4820    fn test_compile_object_destructuring_simple() {
4821        let code = r#"
4822            const obj = { id: 1, name: "Alice" };
4823            const { id, name } = obj;
4824            return { id, name };
4825        "#;
4826        let mut compiler = PlanCompiler::new();
4827        let plan = compiler
4828            .compile_code(code)
4829            .expect("Object destructuring should compile");
4830        // Should have: Assign(obj), Assign(__destructure_0), Assign(id), Assign(name), Return
4831        assert!(plan.steps.len() >= 4);
4832    }
4833
4834    #[tokio::test]
4835    async fn test_execute_object_destructuring_simple() {
4836        let code = r#"
4837            const obj = { id: 1, name: "Alice", extra: "ignored" };
4838            const { id, name } = obj;
4839            return { id, name };
4840        "#;
4841        let mut compiler = PlanCompiler::new();
4842        let plan = compiler.compile_code(code).unwrap();
4843        let mock_http = MockHttpExecutor::new();
4844        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4845        let result = executor.execute(&plan).await.unwrap();
4846        assert_eq!(result.value["id"], serde_json::json!(1));
4847        assert_eq!(result.value["name"], serde_json::json!("Alice"));
4848        // "extra" should not be in output since we only destructured id and name
4849        assert!(result.value.get("extra").is_none());
4850    }
4851
4852    #[tokio::test]
4853    async fn test_execute_object_destructuring_renamed() {
4854        let code = r#"
4855            const user = { id: 1, name: "Alice" };
4856            const { id: userId, name: userName } = user;
4857            return { userId, userName };
4858        "#;
4859        let mut compiler = PlanCompiler::new();
4860        let plan = compiler.compile_code(code).unwrap();
4861        let mock_http = MockHttpExecutor::new();
4862        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4863        let result = executor.execute(&plan).await.unwrap();
4864        assert_eq!(result.value["userId"], serde_json::json!(1));
4865        assert_eq!(result.value["userName"], serde_json::json!("Alice"));
4866    }
4867
4868    #[tokio::test]
4869    async fn test_execute_object_destructuring_with_api_call() {
4870        // The primary use case: destructure API response
4871        let code = r#"
4872            const { data, status } = await api.get('/users');
4873            return { data, status };
4874        "#;
4875        let mut compiler = PlanCompiler::new();
4876        let plan = compiler.compile_code(code).unwrap();
4877        let mut mock_http = MockHttpExecutor::new();
4878        mock_http.add_response(
4879            "/users",
4880            serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
4881        );
4882        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4883        let result = executor.execute(&plan).await.unwrap();
4884        assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
4885        assert_eq!(result.value["status"], serde_json::json!("ok"));
4886    }
4887
4888    #[tokio::test]
4889    async fn test_execute_object_destructuring_missing_property() {
4890        // Missing properties should be null (matches JS behavior)
4891        let code = r#"
4892            const obj = { id: 1 };
4893            const { id, name } = obj;
4894            return { id, name };
4895        "#;
4896        let mut compiler = PlanCompiler::new();
4897        let plan = compiler.compile_code(code).unwrap();
4898        let mock_http = MockHttpExecutor::new();
4899        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4900        let result = executor.execute(&plan).await.unwrap();
4901        assert_eq!(result.value["id"], serde_json::json!(1));
4902        assert_eq!(result.value["name"], serde_json::json!(null));
4903    }
4904
4905    // =========================================================================
4906    // Array Destructuring Tests
4907    // =========================================================================
4908
4909    #[tokio::test]
4910    async fn test_execute_array_destructuring_simple() {
4911        let code = r#"
4912            const arr = [10, 20, 30];
4913            const [a, b] = arr;
4914            return { a, b };
4915        "#;
4916        let mut compiler = PlanCompiler::new();
4917        let plan = compiler.compile_code(code).unwrap();
4918        let mock_http = MockHttpExecutor::new();
4919        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4920        let result = executor.execute(&plan).await.unwrap();
4921        assert_eq!(result.value["a"], serde_json::json!(10));
4922        assert_eq!(result.value["b"], serde_json::json!(20));
4923    }
4924
4925    #[tokio::test]
4926    async fn test_execute_array_destructuring_with_promise_all() {
4927        // Common pattern: destructure Promise.all results
4928        let code = r#"
4929            const [users, products] = await Promise.all([
4930                api.get('/users'),
4931                api.get('/products')
4932            ]);
4933            return { users, products };
4934        "#;
4935        let mut compiler = PlanCompiler::new();
4936        let plan = compiler.compile_code(code).unwrap();
4937        let mut mock_http = MockHttpExecutor::new();
4938        mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
4939        mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
4940        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4941        let result = executor.execute(&plan).await.unwrap();
4942        assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
4943        assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
4944    }
4945
4946    // =========================================================================
4947    // For-of Loop Destructuring Tests
4948    // =========================================================================
4949
4950    #[test]
4951    fn test_compile_for_of_destructuring() {
4952        let code = r#"
4953            const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4954            const results = [];
4955            for (const { id, name } of items.slice(0, 10)) {
4956                results.push({ id, name });
4957            }
4958            return results;
4959        "#;
4960        let mut compiler = PlanCompiler::new();
4961        compiler
4962            .compile_code(code)
4963            .expect("For-of with destructuring should compile");
4964    }
4965
4966    #[tokio::test]
4967    async fn test_execute_for_of_destructuring() {
4968        let code = r#"
4969            const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4970            const results = [];
4971            for (const { id, name } of items.slice(0, 10)) {
4972                results.push({ label: name, num: id });
4973            }
4974            return results;
4975        "#;
4976        let mut compiler = PlanCompiler::new();
4977        let plan = compiler.compile_code(code).unwrap();
4978        let mock_http = MockHttpExecutor::new();
4979        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4980        let result = executor.execute(&plan).await.unwrap();
4981        let arr = result.value.as_array().unwrap();
4982        assert_eq!(arr.len(), 2);
4983        assert_eq!(arr[0]["label"], serde_json::json!("A"));
4984        assert_eq!(arr[0]["num"], serde_json::json!(1));
4985        assert_eq!(arr[1]["label"], serde_json::json!("B"));
4986        assert_eq!(arr[1]["num"], serde_json::json!(2));
4987    }
4988
4989    #[tokio::test]
4990    async fn test_execute_for_of_destructuring_with_api_calls() {
4991        // Real-world pattern: destructure loop items, use properties in API calls
4992        let code = r#"
4993            const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
4994            const results = [];
4995            for (const { id, role } of users.slice(0, 10)) {
4996                const detail = await api.get(`/users/${id}`);
4997                results.push({ role, detail });
4998            }
4999            return results;
5000        "#;
5001        let mut compiler = PlanCompiler::new();
5002        let plan = compiler.compile_code(code).unwrap();
5003        let mut mock_http = MockHttpExecutor::new();
5004        mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
5005        mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
5006        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5007        let result = executor.execute(&plan).await.unwrap();
5008        let arr = result.value.as_array().unwrap();
5009        assert_eq!(arr.len(), 2);
5010        assert_eq!(arr[0]["role"], serde_json::json!("admin"));
5011        assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
5012        assert_eq!(arr[1]["role"], serde_json::json!("user"));
5013        assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
5014    }
5015
5016    // =========================================================================
5017    // Combined Spread + Destructuring Tests
5018    // =========================================================================
5019
5020    #[tokio::test]
5021    async fn test_execute_spread_and_destructuring_combined() {
5022        // Realistic pattern: destructure API response, spread into new request
5023        let code = r#"
5024            const { data, token } = await api.get('/auth');
5025            const result = await api.post('/action', { ...data, token });
5026            return result;
5027        "#;
5028        let mut compiler = PlanCompiler::new();
5029        let plan = compiler.compile_code(code).unwrap();
5030        let mut mock_http = MockHttpExecutor::new();
5031        mock_http.add_response(
5032            "/auth",
5033            serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
5034        );
5035        mock_http.add_response("/action", serde_json::json!({ "success": true }));
5036        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5037        let result = executor.execute(&plan).await.unwrap();
5038        assert_eq!(result.value["success"], serde_json::json!(true));
5039    }
5040}