Skip to main content

agentos_v8_runtime/
execution.rs

1// Script compilation, CJS/ESM execution, module loading
2
3use std::cell::RefCell;
4use std::collections::{HashMap, HashSet};
5use std::ffi::c_void;
6use std::num::NonZeroI32;
7use std::time::Instant;
8
9// ── Module-load read/compile split (opt-in via AGENTOS_MODULE_TRACE=1) ──
10// Per module miss: resolve IPC + load (read) IPC + format IPC + V8 compile.
11// Accumulates ns per category and writes a running total to
12// AGENTOS_MODULE_TRACE_FILE so we can see whether the VM module-load tax is
13// IPC (read) or V8 compile bound. Index: 0=count 1=resolve 2=load 3=format 4=compile.
14static MOD_TRACE: std::sync::OnceLock<std::sync::Mutex<[u64; 5]>> = std::sync::OnceLock::new();
15
16fn mod_trace_enabled() -> bool {
17    std::env::var("AGENTOS_MODULE_TRACE").as_deref() == Ok("1")
18}
19
20fn record_mod(idx: usize, ns: u64) {
21    let m = MOD_TRACE.get_or_init(|| std::sync::Mutex::new([0u64; 5]));
22    let Ok(mut a) = m.lock() else {
23        return;
24    };
25    a[idx] = a[idx].wrapping_add(ns);
26    if idx == 4 {
27        a[0] += 1;
28        if a[0] % 25 == 0 {
29            if let Ok(path) = std::env::var("AGENTOS_MODULE_TRACE_FILE") {
30                let _ = std::fs::write(
31                    &path,
32                    format!(
33                        "modules={} resolve_ms={} load_ms={} format_ms={} compile_ms={}\n",
34                        a[0],
35                        a[1] / 1_000_000,
36                        a[2] / 1_000_000,
37                        a[3] / 1_000_000,
38                        a[4] / 1_000_000,
39                    ),
40                );
41            }
42        }
43    }
44}
45
46use crate::bridge::{deserialize_v8_value, serialize_v8_value};
47use crate::host_call::BridgeCallContext;
48use crate::ipc::ExecutionError;
49#[cfg(test)]
50use crate::ipc::{OsConfig, ProcessConfig};
51
52/// Cached V8 code cache data for bridge code compilation.
53///
54/// Stores the compiled bytecode from V8's ScriptCompiler::CreateCodeCache
55/// along with a hash of the source for invalidation. On subsequent
56/// compilations with the same bridge code, the cache is consumed via
57/// CompileOptions::ConsumeCodeCache, skipping parsing and initial compilation.
58pub struct BridgeCodeCache {
59    /// FNV-1a hash of the bridge code source string
60    source_hash: u64,
61    /// Raw code cache bytes from UnboundScript::create_code_cache()
62    cached_data: Vec<u8>,
63}
64
65impl BridgeCodeCache {
66    /// Compute FNV-1a hash of bridge code source
67    fn hash_source(source: &str) -> u64 {
68        let mut hash: u64 = 0xcbf29ce484222325;
69        for byte in source.as_bytes() {
70            hash ^= *byte as u64;
71            hash = hash.wrapping_mul(0x100000001b3);
72        }
73        hash
74    }
75}
76
77/// Inject `_processConfig` and `_osConfig` as frozen, non-writable, non-configurable
78/// global properties, and harden the context (remove SharedArrayBuffer in freeze mode).
79///
80/// Must be called within a ContextScope.
81#[cfg(test)]
82pub fn inject_globals(
83    scope: &mut v8::HandleScope,
84    process_config: &ProcessConfig,
85    os_config: &OsConfig,
86) {
87    let context = scope.get_current_context();
88    let global = context.global(scope);
89    // Build and freeze _processConfig
90    let pc_obj = build_process_config(scope, process_config);
91    pc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
92    let pc_key = v8::String::new(scope, "_processConfig").unwrap();
93    let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
94    global.define_own_property(scope, pc_key.into(), pc_obj.into(), attr);
95
96    // Build and freeze _osConfig
97    let os_obj = build_os_config(scope, os_config);
98    os_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
99    let os_key = v8::String::new(scope, "_osConfig").unwrap();
100    let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
101    global.define_own_property(scope, os_key.into(), os_obj.into(), attr);
102
103    // SharedArrayBuffer removal for timing mitigation is handled by the JS-side
104    // bridge code (applyTimingMitigationFreeze), which runs AFTER the bridge bundle
105    // loads. The bridge bundle depends on SharedArrayBuffer being available during
106    // its initialization (whatwg-url/webidl-conversions uses it).
107}
108
109pub fn install_high_resolution_time_global(scope: &mut v8::HandleScope, origin: *const Instant) {
110    let context = scope.get_current_context();
111    let global = context.global(scope);
112    let external = v8::External::new(scope, origin as *mut c_void);
113    let template = v8::FunctionTemplate::builder(high_resolution_time_callback)
114        .data(external.into())
115        .build(scope);
116    let Some(func) = template.get_function(scope) else {
117        return;
118    };
119    let key = v8::String::new(scope, "__secureExecHrNowUs").unwrap();
120    let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
121    global.define_own_property(scope, key.into(), func.into(), attr);
122}
123
124fn high_resolution_time_callback(
125    scope: &mut v8::HandleScope,
126    args: v8::FunctionCallbackArguments,
127    mut rv: v8::ReturnValue,
128) {
129    let external = match v8::Local::<v8::External>::try_from(args.data()) {
130        Ok(ext) => ext,
131        Err(_) => {
132            let msg = v8::String::new(scope, "internal error: missing hrtime origin").unwrap();
133            let exc = v8::Exception::error(scope, msg);
134            scope.throw_exception(exc);
135            return;
136        }
137    };
138    // SAFETY: the pointer targets the session thread's per-isolate Instant,
139    // which is kept alive for the lifetime of the V8 session thread.
140    let origin = unsafe { &*(external.value() as *const Instant) };
141    let micros = origin.elapsed().as_secs_f64() * 1_000_000.0;
142    rv.set(v8::Number::new(scope, micros).into());
143}
144
145/// Inject globals from a V8-serialized payload containing { processConfig, osConfig }.
146///
147/// The payload is produced by node:v8.serialize() on the host side.
148/// Deserializes into V8, extracts processConfig and osConfig, freezes them,
149/// and sets them as non-writable, non-configurable global properties.
150pub fn inject_globals_from_payload(
151    scope: &mut v8::HandleScope,
152    payload: &[u8],
153) -> Result<(), ExecutionError> {
154    let context = scope.get_current_context();
155    let global = context.global(scope);
156
157    // Deserialize the V8 payload { processConfig, osConfig }
158    let config_val = deserialize_v8_value(scope, payload)
159        .map_err(|err| invalid_globals_payload_error(format!("decode failed: {err}")))?;
160
161    if !config_val.is_object() {
162        return Err(invalid_globals_payload_error("payload is not an object"));
163    }
164    let config_obj = v8::Local::<v8::Object>::try_from(config_val)
165        .map_err(|_| invalid_globals_payload_error("payload is not an object"))?;
166    if !is_plain_config_object(scope, config_obj) {
167        return Err(invalid_globals_payload_error(
168            "payload is not a plain object",
169        ));
170    }
171
172    // Validate both config objects before mutating globals so malformed payloads
173    // cannot leave a partially injected execution context.
174    let (pc_val, pc_obj) = required_object_property(scope, config_obj, "processConfig")?;
175    let (oc_val, oc_obj) = required_object_property(scope, config_obj, "osConfig")?;
176
177    let (_env_val, env_obj) =
178        required_object_property_with_label(scope, pc_obj, "env", "processConfig.env")?;
179    freeze_config_object(scope, env_obj, "processConfig.env")?;
180    freeze_config_object(scope, pc_obj, "processConfig")?;
181    freeze_config_object(scope, oc_obj, "osConfig")?;
182    let global_key = v8::String::new(scope, "_processConfig").unwrap();
183    let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
184    global.define_own_property(scope, global_key.into(), pc_val, attr);
185
186    let global_key = v8::String::new(scope, "_osConfig").unwrap();
187    let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
188    global.define_own_property(scope, global_key.into(), oc_val, attr);
189
190    Ok(())
191}
192
193fn required_object_property<'s>(
194    scope: &mut v8::HandleScope<'s>,
195    obj: v8::Local<'s, v8::Object>,
196    name: &str,
197) -> Result<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>), ExecutionError> {
198    required_object_property_with_label(scope, obj, name, name)
199}
200
201fn required_object_property_with_label<'s>(
202    scope: &mut v8::HandleScope<'s>,
203    obj: v8::Local<'s, v8::Object>,
204    name: &str,
205    error_label: &str,
206) -> Result<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>), ExecutionError> {
207    let key = v8::String::new(scope, name).unwrap();
208    let value = obj
209        .get(scope, key.into())
210        .filter(|value| !value.is_null_or_undefined())
211        .ok_or_else(|| invalid_globals_payload_error(format!("missing {error_label}")))?;
212    if !value.is_object() {
213        return Err(invalid_globals_payload_error(format!(
214            "{error_label} is not an object"
215        )));
216    }
217    let object = v8::Local::<v8::Object>::try_from(value)
218        .map_err(|_| invalid_globals_payload_error(format!("{error_label} is not an object")))?;
219    if !is_plain_config_object(scope, object) {
220        return Err(invalid_globals_payload_error(format!(
221            "{error_label} is not a plain object"
222        )));
223    }
224    Ok((value, object))
225}
226
227fn is_plain_config_object(scope: &mut v8::HandleScope, object: v8::Local<v8::Object>) -> bool {
228    let Some(prototype) = object.get_prototype(scope) else {
229        return false;
230    };
231    if prototype.is_null() {
232        return true;
233    }
234    if !prototype.is_object() {
235        return false;
236    }
237    let Ok(prototype_object) = v8::Local::<v8::Object>::try_from(prototype) else {
238        return false;
239    };
240    prototype_object
241        .get_prototype(scope)
242        .is_some_and(|parent| parent.is_null())
243}
244
245fn freeze_config_object(
246    scope: &mut v8::HandleScope,
247    object: v8::Local<v8::Object>,
248    label: &str,
249) -> Result<(), ExecutionError> {
250    match object.set_integrity_level(scope, v8::IntegrityLevel::Frozen) {
251        Some(true) => Ok(()),
252        Some(false) | None => Err(invalid_globals_payload_error(format!(
253            "failed to freeze {label}"
254        ))),
255    }
256}
257
258fn invalid_globals_payload_error(message: impl Into<String>) -> ExecutionError {
259    ExecutionError {
260        error_type: "Error".into(),
261        message: format!("invalid InjectGlobals payload: {}", message.into()),
262        stack: String::new(),
263        code: Some("ERR_INVALID_GLOBALS_PAYLOAD".into()),
264    }
265}
266
267/// Compile and run bridge code as a V8 Script, using code cache if available.
268///
269/// On cache miss (first compilation or hash mismatch): compiles with
270/// NoCompileOptions and creates a code cache from the resulting UnboundScript.
271/// On cache hit: compiles with ConsumeCodeCache using the cached bytecode.
272/// Creates its own TryCatch scope internally so the caller's scope is released.
273/// Returns (exit_code, error) — exit code 0 on success.
274fn run_bridge_cached(
275    scope: &mut v8::HandleScope,
276    bridge_code: &str,
277    cache: &mut Option<BridgeCodeCache>,
278) -> (i32, Option<ExecutionError>) {
279    let tc = &mut v8::TryCatch::new(scope);
280
281    let v8_source = match v8::String::new(tc, bridge_code) {
282        Some(s) => s,
283        None => {
284            return (
285                1,
286                Some(ExecutionError {
287                    error_type: "Error".into(),
288                    message: "bridge code string too large for V8".into(),
289                    stack: String::new(),
290                    code: None,
291                }),
292            );
293        }
294    };
295
296    // Resource name for bridge code (needed for code cache to work)
297    let resource_name = v8::String::new(tc, "<bridge>").unwrap();
298    let origin = v8::ScriptOrigin::new(
299        tc,
300        resource_name.into(),
301        0,
302        0,
303        false,
304        -1,
305        None,
306        false,
307        false,
308        false,
309        None,
310    );
311
312    let source_hash = BridgeCodeCache::hash_source(bridge_code);
313
314    // Check if cache is valid for this bridge code
315    let cache_hit = cache.as_ref().is_some_and(|c| c.source_hash == source_hash);
316
317    let script = if cache_hit {
318        // Consume cached bytecode
319        let cached_bytes = &cache.as_ref().unwrap().cached_data;
320        let cached_data = v8::script_compiler::CachedData::new(cached_bytes);
321        let mut source = v8::script_compiler::Source::new_with_cached_data(
322            v8_source,
323            Some(&origin),
324            cached_data,
325        );
326        let compiled = v8::script_compiler::compile(
327            tc,
328            &mut source,
329            v8::script_compiler::CompileOptions::ConsumeCodeCache,
330            v8::script_compiler::NoCacheReason::NoReason,
331        );
332        // If cache was rejected, invalidate it (will be regenerated next time)
333        if source.get_cached_data().is_some_and(|cd| cd.rejected()) {
334            *cache = None;
335        }
336        compiled
337    } else {
338        // First compilation or cache invalidated — compile without cache
339        let mut source = v8::script_compiler::Source::new(v8_source, Some(&origin));
340        let compiled = v8::script_compiler::compile(
341            tc,
342            &mut source,
343            v8::script_compiler::CompileOptions::NoCompileOptions,
344            v8::script_compiler::NoCacheReason::NoReason,
345        );
346        // Generate code cache from the compiled script
347        if let Some(ref script) = compiled {
348            let unbound = script.get_unbound_script(tc);
349            if let Some(code_cache) = unbound.create_code_cache() {
350                *cache = Some(BridgeCodeCache {
351                    source_hash,
352                    cached_data: code_cache.to_vec(),
353                });
354            }
355        }
356        compiled
357    };
358
359    // Run the compiled script
360    let script = match script {
361        Some(s) => s,
362        None => {
363            return match tc.exception() {
364                Some(e) => {
365                    let (c, err) = exception_to_result(tc, e);
366                    (c, Some(err))
367                }
368                None => (1, None),
369            };
370        }
371    };
372
373    if script.run(tc).is_none() {
374        return match tc.exception() {
375            Some(e) => {
376                let (c, err) = exception_to_result(tc, e);
377                (c, Some(err))
378            }
379            None => (1, None),
380        };
381    }
382
383    (0, None)
384}
385
386/// Run a short init script (e.g. post-restore config). Compiles and executes
387/// via v8::Script, returning (exit_code, error) on failure. No code caching.
388#[cfg(not(test))]
389pub fn run_init_script(scope: &mut v8::HandleScope, code: &str) -> (i32, Option<ExecutionError>) {
390    if code.is_empty() {
391        return (0, None);
392    }
393    let tc = &mut v8::TryCatch::new(scope);
394    let source = match v8::String::new(tc, code) {
395        Some(s) => s,
396        None => {
397            return (
398                1,
399                Some(ExecutionError {
400                    error_type: "Error".into(),
401                    message: "init script string too large for V8".into(),
402                    stack: String::new(),
403                    code: None,
404                }),
405            );
406        }
407    };
408    let script = match v8::Script::compile(tc, source, None) {
409        Some(s) => s,
410        None => {
411            return match tc.exception() {
412                Some(e) => {
413                    let (c, err) = exception_to_result(tc, e);
414                    (c, Some(err))
415                }
416                None => (1, None),
417            };
418        }
419    };
420    if script.run(tc).is_none() {
421        return match tc.exception() {
422            Some(e) => {
423                let (c, err) = exception_to_result(tc, e);
424                (c, Some(err))
425            }
426            None => (1, None),
427        };
428    }
429    (0, None)
430}
431
432/// Execute user code as a CJS script (mode='exec').
433///
434/// Runs bridge_code as IIFE first (if non-empty), then compiles and runs user_code
435/// via v8::Script. Returns (exit_code, error) — exit code 0 on success, 1 on error.
436/// The `bridge_cache` parameter enables code caching for repeated bridge compilations.
437pub fn execute_script(
438    scope: &mut v8::HandleScope,
439    bridge_code: &str,
440    user_code: &str,
441    bridge_cache: &mut Option<BridgeCodeCache>,
442) -> (i32, Option<ExecutionError>) {
443    execute_script_with_options(scope, None, bridge_code, user_code, None, bridge_cache)
444}
445
446pub fn execute_script_with_options(
447    scope: &mut v8::HandleScope,
448    bridge_ctx: Option<&BridgeCallContext>,
449    bridge_code: &str,
450    user_code: &str,
451    file_path: Option<&str>,
452    bridge_cache: &mut Option<BridgeCodeCache>,
453) -> (i32, Option<ExecutionError>) {
454    if let Some(bridge_ctx) = bridge_ctx {
455        MODULE_RESOLVE_STATE.with(|cell| {
456            *cell.borrow_mut() = Some(ModuleResolveState {
457                bridge_ctx: bridge_ctx as *const BridgeCallContext,
458                module_names: HashMap::new(),
459                module_cache: HashMap::new(),
460                guest_reader: None,
461            });
462        });
463    }
464
465    // Run bridge code IIFE (with code caching)
466    if !bridge_code.is_empty() {
467        let (code, err) = run_bridge_cached(scope, bridge_code, bridge_cache);
468        if code != 0 {
469            if bridge_ctx.is_some() {
470                clear_module_state();
471            }
472            return (code, err);
473        }
474    }
475
476    // Run user code
477    {
478        let tc = &mut v8::TryCatch::new(scope);
479        let source = match v8::String::new(tc, user_code) {
480            Some(s) => s,
481            None => {
482                if bridge_ctx.is_some() {
483                    clear_module_state();
484                }
485                return (
486                    1,
487                    Some(ExecutionError {
488                        error_type: "Error".into(),
489                        message: "user code string too large for V8".into(),
490                        stack: String::new(),
491                        code: None,
492                    }),
493                );
494            }
495        };
496        let origin = file_path.and_then(|path| {
497            let resource = v8::String::new(tc, path)?;
498            Some(v8::ScriptOrigin::new(
499                tc,
500                resource.into(),
501                0,
502                0,
503                false,
504                -1,
505                None,
506                false,
507                false,
508                false,
509                None,
510            ))
511        });
512        let script = match v8::Script::compile(tc, source, origin.as_ref()) {
513            Some(s) => s,
514            None => {
515                if bridge_ctx.is_some() {
516                    clear_module_state();
517                }
518                return match tc.exception() {
519                    Some(e) => {
520                        let (c, err) = exception_to_result(tc, e);
521                        (c, Some(err))
522                    }
523                    None => (1, None),
524                };
525            }
526        };
527        let completion = match script.run(tc) {
528            Some(result) => result,
529            None => {
530                if bridge_ctx.is_some() {
531                    clear_module_state();
532                }
533                return match tc.exception() {
534                    Some(e) => {
535                        let (c, err) = exception_to_result(tc, e);
536                        (c, Some(err))
537                    }
538                    None => (1, None),
539                };
540            }
541        };
542
543        // Flush microtasks once after every exec()-style script so process.nextTick()
544        // and zero-delay bridge callbacks run before we decide whether more event-loop
545        // work is pending.
546        tc.perform_microtask_checkpoint();
547
548        if let Some(exception) = tc.exception() {
549            if bridge_ctx.is_some() {
550                clear_module_state();
551            }
552            let (c, err) = exception_to_result(tc, exception);
553            return (c, Some(err));
554        }
555
556        if let Some(err) = take_unhandled_promise_rejection(tc) {
557            if bridge_ctx.is_some() {
558                clear_module_state();
559            }
560            return (1, Some(err));
561        }
562
563        // Surface rejected async completions for exec()-style scripts that
564        // return a Promise (for example an async IIFE ending in await import()).
565        if completion.is_promise() {
566            let promise = v8::Local::<v8::Promise>::try_from(completion).unwrap();
567            match promise.state() {
568                v8::PromiseState::Pending => {
569                    set_pending_script_evaluation(tc, promise);
570                    return (0, None);
571                }
572                v8::PromiseState::Rejected => {
573                    let rejection = promise.result(tc);
574                    if bridge_ctx.is_some() {
575                        clear_module_state();
576                    }
577                    let (c, err) = exception_to_result(tc, rejection);
578                    return (c, Some(err));
579                }
580                v8::PromiseState::Fulfilled => {
581                    return (extract_global_process_exit_code(tc).unwrap_or(0), None);
582                }
583            }
584        }
585    }
586
587    (extract_global_process_exit_code(scope).unwrap_or(0), None)
588}
589
590/// Check if a V8 exception is a ProcessExitError (has `_isProcessExit: true` sentinel).
591/// Returns `Some(exit_code)` if detected, `None` otherwise.
592///
593/// ProcessExitError is detected by sentinel property, not by regex matching on the
594/// error message or constructor name.
595pub fn extract_process_exit_code(
596    scope: &mut v8::HandleScope,
597    exception: v8::Local<v8::Value>,
598) -> Option<i32> {
599    if !exception.is_object() {
600        return None;
601    }
602    let obj = v8::Local::<v8::Object>::try_from(exception).ok()?;
603    let sentinel_key = v8::String::new(scope, "_isProcessExit")?;
604    let sentinel_val = obj.get(scope, sentinel_key.into())?;
605    if !sentinel_val.is_true() {
606        return None;
607    }
608    // Extract numeric exit code from .code property
609    let code_key = v8::String::new(scope, "code")?;
610    let code_val = obj.get(scope, code_key.into())?;
611    if code_val.is_undefined() || code_val.is_null() {
612        Some(0)
613    } else if code_val.is_number() {
614        Some(code_val.int32_value(scope).unwrap_or(0))
615    } else {
616        Some(1)
617    }
618}
619
620fn extract_global_process_exit_code(scope: &mut v8::HandleScope) -> Option<i32> {
621    let context = scope.get_current_context();
622    let global = context.global(scope);
623    let process_key = v8::String::new(scope, "process")?;
624    let process_val = global.get(scope, process_key.into())?;
625    if !process_val.is_object() {
626        return None;
627    }
628
629    let process_obj = v8::Local::<v8::Object>::try_from(process_val).ok()?;
630    let exit_code_key = v8::String::new(scope, "exitCode")?;
631    let exit_code_val = process_obj.get(scope, exit_code_key.into())?;
632    if exit_code_val.is_undefined() || exit_code_val.is_null() {
633        None
634    } else if exit_code_val.is_number() {
635        Some(exit_code_val.int32_value(scope).unwrap_or(0))
636    } else {
637        None
638    }
639}
640
641/// Extract error info and exit code from a V8 exception.
642/// For ProcessExitError (detected via _isProcessExit sentinel), returns the error's exit code.
643/// For other errors, returns exit code 1.
644pub(crate) fn exception_to_result(
645    scope: &mut v8::HandleScope,
646    exception: v8::Local<v8::Value>,
647) -> (i32, ExecutionError) {
648    let error = extract_error_info(scope, exception);
649    let exit_code = extract_process_exit_code(scope, exception)
650        .or_else(|| parse_process_exit_code_from_error(&error))
651        .unwrap_or(1);
652    (exit_code, error)
653}
654
655fn parse_process_exit_code_from_error(error: &ExecutionError) -> Option<i32> {
656    if error.error_type != "ProcessExitError" && !error.message.starts_with("process.exit(") {
657        return None;
658    }
659    let code = error
660        .message
661        .strip_prefix("process.exit(")?
662        .strip_suffix(')')?;
663    code.parse::<i32>().ok()
664}
665
666/// Extract structured error information from a V8 exception value.
667///
668/// Reads constructor.name for error type, .message for the message,
669/// .stack for the stack trace, and optional .code for Node-style error codes.
670pub(crate) fn extract_error_info(
671    scope: &mut v8::HandleScope,
672    exception: v8::Local<v8::Value>,
673) -> ExecutionError {
674    if !exception.is_object() {
675        // Non-object throw (e.g., `throw "string"`)
676        return ExecutionError {
677            error_type: "Error".into(),
678            message: exception.to_rust_string_lossy(scope),
679            stack: String::new(),
680            code: None,
681        };
682    }
683
684    let obj = v8::Local::<v8::Object>::try_from(exception).unwrap();
685
686    // Error type from constructor.name
687    let error_type = {
688        let ctor_key = v8::String::new(scope, "constructor").unwrap();
689        let name_key = v8::String::new(scope, "name").unwrap();
690        obj.get(scope, ctor_key.into())
691            .filter(|v| v.is_object())
692            .and_then(|ctor| {
693                let ctor_obj = v8::Local::<v8::Object>::try_from(ctor).ok()?;
694                ctor_obj.get(scope, name_key.into())
695            })
696            .filter(|v| v.is_string())
697            .map(|v| v.to_rust_string_lossy(scope))
698            .filter(|n| !n.is_empty())
699            .unwrap_or_else(|| "Error".into())
700    };
701
702    // Message from error.message property
703    let message = {
704        let msg_key = v8::String::new(scope, "message").unwrap();
705        obj.get(scope, msg_key.into())
706            .filter(|v| v.is_string())
707            .map(|v| v.to_rust_string_lossy(scope))
708            .unwrap_or_else(|| exception.to_rust_string_lossy(scope))
709    };
710
711    // Stack trace from error.stack property
712    let stack = {
713        let stack_key = v8::String::new(scope, "stack").unwrap();
714        obj.get(scope, stack_key.into())
715            .filter(|v| v.is_string())
716            .map(|v| v.to_rust_string_lossy(scope))
717            .unwrap_or_default()
718    };
719
720    // Optional error code (e.g., ERR_MODULE_NOT_FOUND)
721    let code = {
722        let code_key = v8::String::new(scope, "code").unwrap();
723        obj.get(scope, code_key.into())
724            .filter(|v| v.is_string())
725            .map(|v| v.to_rust_string_lossy(scope))
726    };
727
728    ExecutionError {
729        error_type,
730        message,
731        stack,
732        code,
733    }
734}
735
736/// Build the _processConfig JS object: { cwd, env, timing_mitigation, frozen_time_ms, high_resolution_time }
737#[cfg(test)]
738fn build_process_config<'s>(
739    scope: &mut v8::HandleScope<'s>,
740    config: &ProcessConfig,
741) -> v8::Local<'s, v8::Object> {
742    let obj = v8::Object::new(scope);
743
744    // cwd
745    let key = v8::String::new(scope, "cwd").unwrap();
746    let val = v8::String::new(scope, &config.cwd).unwrap();
747    obj.set(scope, key.into(), val.into());
748
749    // env (frozen sub-object)
750    let env_key = v8::String::new(scope, "env").unwrap();
751    let env_obj = v8::Object::new(scope);
752    for (k, v) in &config.env {
753        let ek = v8::String::new(scope, k).unwrap();
754        let ev = v8::String::new(scope, v).unwrap();
755        env_obj.set(scope, ek.into(), ev.into());
756    }
757    env_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
758    obj.set(scope, env_key.into(), env_obj.into());
759
760    // timing_mitigation
761    let key = v8::String::new(scope, "timing_mitigation").unwrap();
762    let val = v8::String::new(scope, &config.timing_mitigation).unwrap();
763    obj.set(scope, key.into(), val.into());
764
765    // frozen_time_ms (number or null)
766    let key = v8::String::new(scope, "frozen_time_ms").unwrap();
767    let val: v8::Local<v8::Value> = match config.frozen_time_ms {
768        Some(ms) => v8::Number::new(scope, ms).into(),
769        None => v8::null(scope).into(),
770    };
771    obj.set(scope, key.into(), val);
772
773    // high_resolution_time
774    let key = v8::String::new(scope, "high_resolution_time").unwrap();
775    let val = v8::Boolean::new(scope, config.high_resolution_time);
776    obj.set(scope, key.into(), val.into());
777
778    obj
779}
780
781/// Build the _osConfig JS object: { homedir, tmpdir, platform, arch }
782#[cfg(test)]
783fn build_os_config<'s>(
784    scope: &mut v8::HandleScope<'s>,
785    config: &OsConfig,
786) -> v8::Local<'s, v8::Object> {
787    let obj = v8::Object::new(scope);
788
789    for (name, value) in [
790        ("homedir", config.homedir.as_str()),
791        ("tmpdir", config.tmpdir.as_str()),
792        ("platform", config.platform.as_str()),
793        ("arch", config.arch.as_str()),
794    ] {
795        let key = v8::String::new(scope, name).unwrap();
796        let val = v8::String::new(scope, value).unwrap();
797        obj.set(scope, key.into(), val.into());
798    }
799
800    obj
801}
802
803// --- ESM module loading ---
804
805/// Thread-local state for module resolution during execute_module.
806/// Avoids passing user data through V8's ResolveModuleCallback (which is a plain fn pointer).
807/// Direct, in-process module source reader living on the V8 session thread.
808///
809/// The V8 module callback (resolve_or_compile_module) runs in this crate
810/// (v8-runtime), but the module reader/resolver lives in the higher `execution`
811/// crate, so a direct call would be a circular dependency — today every module
812/// resolve/load/format is a sync bridge round-trip (~139us × ~5,100 calls ≈ all
813/// of loadPiSdkRuntime). This trait is owned here and implemented in the higher
814/// crate over the mounted `HostDirModuleReader`, then handed down to the session
815/// thread so module source can be read directly, skipping the round-trip. It is
816/// confined to the same mounts the guest sees (the impl keeps the reader's
817/// `openat2(RESOLVE_BENEATH)` confinement).
818pub trait GuestModuleReader: Send {
819    /// Read the source for an already-resolved guest module path, or `None` if
820    /// the path isn't served by this reader (caller falls back to the bridge IPC).
821    fn read_module_source(&mut self, resolved_guest_path: &str) -> Option<String>;
822
823    /// Resolve a module specifier (import mode) to a resolved guest path directly,
824    /// skipping the bridge `_resolveModule` round-trip. `None` => fall back to IPC.
825    /// Implementations must match the bridge resolver exactly (same cache, same
826    /// ESM/CJS/exports/symlink semantics).
827    fn resolve_module(&mut self, specifier: &str, referrer: &str) -> Option<String> {
828        let _ = (specifier, referrer);
829        None
830    }
831}
832
833/// Install (or clear) the direct module reader for the current session thread.
834/// Called by the session thread when it receives a `SetModuleReader` command; the
835/// next `execute_module` moves it into the resolve state. Must be called on the
836/// session/isolate thread.
837pub fn install_session_guest_reader(reader: Option<Box<dyn GuestModuleReader>>) {
838    SESSION_GUEST_READER.with(|cell| *cell.borrow_mut() = reader);
839}
840
841struct ModuleResolveState {
842    bridge_ctx: *const BridgeCallContext,
843    /// identity_hash → resource_name for referrer lookup
844    module_names: HashMap<NonZeroI32, String>,
845    /// resolved_path and referrer-qualified request keys → Global<Module> cache
846    module_cache: HashMap<String, v8::Global<v8::Module>>,
847    /// Optional direct module-source reader (session-thread local). When present,
848    /// module loads read source directly instead of via the bridge round-trip.
849    guest_reader: Option<Box<dyn GuestModuleReader>>,
850}
851
852// SAFETY: ModuleResolveState is only accessed from the session thread
853// (single-threaded per session). The raw pointer is valid for the
854// duration of execute_module.
855unsafe impl Send for ModuleResolveState {}
856
857/// Deferred root-module completion state for async ESM evaluation.
858///
859/// When `module.evaluate()` returns a pending promise (for example because the
860/// entry module or one of its dependencies uses top-level `await`), the session
861/// thread keeps the module + promise alive across the bridge event loop and
862/// finalizes exports only after the promise settles.
863#[cfg_attr(test, allow(dead_code))]
864struct PendingModuleEvaluation {
865    module: v8::Global<v8::Module>,
866    promise: v8::Global<v8::Promise>,
867}
868
869// SAFETY: PendingModuleEvaluation is only accessed from the session thread
870// (single-threaded per session).
871unsafe impl Send for PendingModuleEvaluation {}
872
873struct PendingScriptEvaluation {
874    promise: v8::Global<v8::Promise>,
875}
876
877unsafe impl Send for PendingScriptEvaluation {}
878
879thread_local! {
880    static MODULE_RESOLVE_STATE: RefCell<Option<ModuleResolveState>> = const { RefCell::new(None) };
881    /// Session-thread-local handoff: a SetModuleReader command stashes the reader
882    /// here, and the next execute_module moves it into ModuleResolveState so module
883    /// source loads read directly on this thread instead of round-tripping the bridge.
884    static SESSION_GUEST_READER: RefCell<Option<Box<dyn GuestModuleReader>>> = const { RefCell::new(None) };
885    static PENDING_MODULE_EVALUATION: RefCell<Option<PendingModuleEvaluation>> = const { RefCell::new(None) };
886    static PENDING_SCRIPT_EVALUATION: RefCell<Option<PendingScriptEvaluation>> = const { RefCell::new(None) };
887    static CJS_RUNTIME_EXTRACTION_IN_PROGRESS: RefCell<HashSet<String>> =
888        RefCell::new(HashSet::new());
889}
890
891const MAX_MODULE_RESOLVE_MODULES: usize = 1024;
892const MAX_MODULE_RESOLVE_CACHE_ENTRIES: usize = 4096;
893const MAX_MODULE_PREFETCH_GRAPH_MODULES: usize = 1024;
894const MAX_MODULE_PREFETCH_BATCH_SIZE: usize = 256;
895const MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES: usize = 16 * 1024 * 1024;
896const MAX_CJS_NAMED_EXPORTS: usize = 1024;
897const MAX_CJS_RUNTIME_EXPORT_NAME_LEN: usize = 512;
898
899fn module_request_cache_key(specifier: &str, referrer_name: &str) -> String {
900    format!("{}\0{}", referrer_name, specifier)
901}
902
903#[cfg_attr(test, allow(dead_code))]
904pub fn clear_module_state() {
905    MODULE_RESOLVE_STATE.with(|cell| {
906        *cell.borrow_mut() = None;
907    });
908}
909
910pub fn clear_pending_module_evaluation() {
911    PENDING_MODULE_EVALUATION.with(|cell| {
912        *cell.borrow_mut() = None;
913    });
914}
915
916pub fn clear_pending_script_evaluation() {
917    PENDING_SCRIPT_EVALUATION.with(|cell| {
918        *cell.borrow_mut() = None;
919    });
920}
921
922#[cfg_attr(test, allow(dead_code))]
923pub fn has_pending_module_evaluation() -> bool {
924    PENDING_MODULE_EVALUATION.with(|cell| cell.borrow().is_some())
925}
926
927pub fn has_pending_script_evaluation() -> bool {
928    PENDING_SCRIPT_EVALUATION.with(|cell| cell.borrow().is_some())
929}
930
931pub fn pending_module_evaluation_needs_wait(scope: &mut v8::HandleScope) -> bool {
932    PENDING_MODULE_EVALUATION.with(|cell| {
933        let borrow = cell.borrow();
934        let Some(pending) = borrow.as_ref() else {
935            return false;
936        };
937        let promise = v8::Local::new(scope, &pending.promise);
938        promise.state() == v8::PromiseState::Pending
939    })
940}
941
942pub fn pending_script_evaluation_needs_wait(scope: &mut v8::HandleScope) -> bool {
943    PENDING_SCRIPT_EVALUATION.with(|cell| {
944        let borrow = cell.borrow();
945        let Some(pending) = borrow.as_ref() else {
946            return false;
947        };
948        let promise = v8::Local::new(scope, &pending.promise);
949        promise.state() == v8::PromiseState::Pending
950    })
951}
952
953fn set_pending_module_evaluation(
954    scope: &mut v8::HandleScope,
955    module: v8::Local<v8::Module>,
956    promise: v8::Local<v8::Promise>,
957) {
958    PENDING_MODULE_EVALUATION.with(|cell| {
959        *cell.borrow_mut() = Some(PendingModuleEvaluation {
960            module: v8::Global::new(scope, module),
961            promise: v8::Global::new(scope, promise),
962        });
963    });
964}
965
966pub fn set_pending_script_evaluation(scope: &mut v8::HandleScope, promise: v8::Local<v8::Promise>) {
967    PENDING_SCRIPT_EVALUATION.with(|cell| {
968        *cell.borrow_mut() = Some(PendingScriptEvaluation {
969            promise: v8::Global::new(scope, promise),
970        });
971    });
972}
973
974pub(crate) fn take_unhandled_promise_rejection(
975    scope: &mut v8::HandleScope,
976) -> Option<ExecutionError> {
977    scope
978        .get_slot_mut::<crate::isolate::PromiseRejectState>()
979        .and_then(|state| state.take_next_unhandled())
980}
981
982pub fn finalize_pending_script_evaluation(
983    scope: &mut v8::HandleScope,
984) -> Option<(i32, Option<ExecutionError>)> {
985    let pending = PENDING_SCRIPT_EVALUATION.with(|cell| cell.borrow_mut().take())?;
986    let tc = &mut v8::TryCatch::new(scope);
987    let promise = v8::Local::new(tc, &pending.promise);
988
989    tc.perform_microtask_checkpoint();
990
991    if let Some(exception) = tc.exception() {
992        let (code, err) = exception_to_result(tc, exception);
993        return Some((code, Some(err)));
994    }
995
996    if let Some(err) = take_unhandled_promise_rejection(tc) {
997        return Some((1, Some(err)));
998    }
999
1000    match promise.state() {
1001        v8::PromiseState::Pending => {
1002            PENDING_SCRIPT_EVALUATION.with(|cell| {
1003                *cell.borrow_mut() = Some(pending);
1004            });
1005            None
1006        }
1007        v8::PromiseState::Rejected => {
1008            let rejection = promise.result(tc);
1009            let (code, err) = exception_to_result(tc, rejection);
1010            Some((code, Some(err)))
1011        }
1012        v8::PromiseState::Fulfilled => {
1013            Some((extract_global_process_exit_code(tc).unwrap_or(0), None))
1014        }
1015    }
1016}
1017
1018fn serialize_module_exports(
1019    scope: &mut v8::HandleScope,
1020    module: v8::Local<v8::Module>,
1021) -> Result<Vec<u8>, ExecutionError> {
1022    // Serialize module namespace (exports)
1023    // If the ESM namespace is empty, fall back to globalThis.module.exports
1024    // for CJS compatibility (code using module.exports = {...}).
1025    // The module namespace is a V8 exotic object that ValueSerializer can't
1026    // handle directly, so we copy its properties into a plain object.
1027    let namespace = module.get_module_namespace();
1028    let namespace_obj = namespace.to_object(scope).unwrap();
1029    let prop_names = namespace_obj
1030        .get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
1031        .unwrap();
1032    let exports_val: v8::Local<v8::Value> = if prop_names.length() == 0 {
1033        // No ESM exports — check CJS module.exports fallback
1034        let ctx = scope.get_current_context();
1035        let global = ctx.global(scope);
1036        let module_key = v8::String::new(scope, "module").unwrap();
1037        let cjs_exports = global
1038            .get(scope, module_key.into())
1039            .and_then(|m| m.to_object(scope))
1040            .and_then(|m| {
1041                let exports_key = v8::String::new(scope, "exports").unwrap();
1042                m.get(scope, exports_key.into())
1043            })
1044            .filter(|v| !v.is_undefined() && !v.is_null_or_undefined());
1045        match cjs_exports {
1046            Some(val) => val,
1047            None => v8::Object::new(scope).into(),
1048        }
1049    } else {
1050        let plain = v8::Object::new(scope);
1051        for i in 0..prop_names.length() {
1052            let key = prop_names.get_index(scope, i).unwrap();
1053            let val = namespace_obj
1054                .get(scope, key)
1055                .unwrap_or_else(|| v8::undefined(scope).into());
1056            plain.set(scope, key, val);
1057        }
1058        plain.into()
1059    };
1060
1061    serialize_v8_value(scope, exports_val).map_err(|err| ExecutionError {
1062        error_type: "Error".into(),
1063        message: format!("failed to serialize exports: {}", err),
1064        stack: String::new(),
1065        code: None,
1066    })
1067}
1068
1069#[cfg_attr(test, allow(dead_code))]
1070pub fn finalize_pending_module_evaluation(
1071    scope: &mut v8::HandleScope,
1072) -> Option<(i32, Option<Vec<u8>>, Option<ExecutionError>)> {
1073    let pending = PENDING_MODULE_EVALUATION.with(|cell| cell.borrow_mut().take())?;
1074    let tc = &mut v8::TryCatch::new(scope);
1075    let module = v8::Local::new(tc, &pending.module);
1076    let promise = v8::Local::new(tc, &pending.promise);
1077
1078    tc.perform_microtask_checkpoint();
1079
1080    if let Some(exception) = tc.exception() {
1081        let (code, err) = exception_to_result(tc, exception);
1082        return Some((code, None, Some(err)));
1083    }
1084
1085    if let Some(err) = take_unhandled_promise_rejection(tc) {
1086        return Some((1, None, Some(err)));
1087    }
1088
1089    match promise.state() {
1090        v8::PromiseState::Pending => {
1091            PENDING_MODULE_EVALUATION.with(|cell| {
1092                *cell.borrow_mut() = Some(pending);
1093            });
1094            None
1095        }
1096        v8::PromiseState::Rejected => {
1097            let rejection = promise.result(tc);
1098            let (code, err) = exception_to_result(tc, rejection);
1099            Some((code, None, Some(err)))
1100        }
1101        v8::PromiseState::Fulfilled => {
1102            if module.get_status() == v8::ModuleStatus::Errored {
1103                let exc = module.get_exception();
1104                let (code, err) = exception_to_result(tc, exc);
1105                return Some((code, None, Some(err)));
1106            }
1107
1108            match serialize_module_exports(tc, module) {
1109                Ok(exports) => Some((0, Some(exports), None)),
1110                Err(err) => Some((1, None, Some(err))),
1111            }
1112        }
1113    }
1114}
1115
1116/// Execute user code as an ES module (mode='run').
1117///
1118/// Runs bridge_code as CJS IIFE first (if non-empty), then compiles and runs
1119/// user_code as a v8::Module. The ResolveModuleCallback sends sync-blocking IPC
1120/// calls via BridgeCallContext to resolve import specifiers and load sources.
1121/// Returns (exit_code, serialized_exports, error).
1122/// The `bridge_cache` parameter enables code caching for repeated bridge compilations.
1123pub fn execute_module(
1124    scope: &mut v8::HandleScope,
1125    bridge_ctx: &BridgeCallContext,
1126    bridge_code: &str,
1127    user_code: &str,
1128    file_path: Option<&str>,
1129    bridge_cache: &mut Option<BridgeCodeCache>,
1130) -> (i32, Option<Vec<u8>>, Option<ExecutionError>) {
1131    clear_pending_module_evaluation();
1132
1133    // Set up thread-local resolve state, taking any reader the session thread
1134    // stashed via a SetModuleReader command so module loads read source directly.
1135    let guest_reader = SESSION_GUEST_READER.with(|cell| cell.borrow_mut().take());
1136    MODULE_RESOLVE_STATE.with(|cell| {
1137        *cell.borrow_mut() = Some(ModuleResolveState {
1138            bridge_ctx: bridge_ctx as *const BridgeCallContext,
1139            module_names: HashMap::new(),
1140            module_cache: HashMap::new(),
1141            guest_reader,
1142        });
1143    });
1144
1145    // Run bridge code IIFE (same as CJS mode, with code caching)
1146    if !bridge_code.is_empty() {
1147        let (code, err) = run_bridge_cached(scope, bridge_code, bridge_cache);
1148        if code != 0 {
1149            clear_module_state();
1150            return (code, None, err);
1151        }
1152    }
1153
1154    // Compile and evaluate as ES module
1155    {
1156        let tc = &mut v8::TryCatch::new(scope);
1157        let resource_name_str = file_path.unwrap_or("<user_module>");
1158        let resource = v8::String::new(tc, resource_name_str).unwrap();
1159        let origin = v8::ScriptOrigin::new(
1160            tc,
1161            resource.into(),
1162            0,
1163            0,
1164            false,
1165            -1,
1166            None,
1167            false,
1168            false,
1169            true, // is_module
1170            None,
1171        );
1172
1173        let effective_user_code = add_esm_runtime_prelude(user_code);
1174        let v8_source = match v8::String::new(tc, &effective_user_code) {
1175            Some(s) => s,
1176            None => {
1177                clear_module_state();
1178                return (
1179                    1,
1180                    None,
1181                    Some(ExecutionError {
1182                        error_type: "Error".into(),
1183                        message: "user code string too large for V8".into(),
1184                        stack: String::new(),
1185                        code: None,
1186                    }),
1187                );
1188            }
1189        };
1190
1191        let mut source = v8::script_compiler::Source::new(v8_source, Some(&origin));
1192        let module = match v8::script_compiler::compile_module(tc, &mut source) {
1193            Some(m) => m,
1194            None => {
1195                clear_module_state();
1196                return match tc.exception() {
1197                    Some(e) => {
1198                        let (c, err) = exception_to_result(tc, e);
1199                        (c, None, Some(err))
1200                    }
1201                    None => (1, None, None),
1202                };
1203            }
1204        };
1205
1206        // Store root module name for referrer lookup in resolve callback
1207        MODULE_RESOLVE_STATE.with(|cell| {
1208            if let Some(state) = cell.borrow_mut().as_mut() {
1209                state
1210                    .module_names
1211                    .insert(module.get_identity_hash(), resource_name_str.to_string());
1212            }
1213        });
1214
1215        // Batch-prefetch static imports (BFS) to reduce IPC round-trips.
1216        // Each level collects uncached specifiers and resolves+loads them in one batch call.
1217        // The resolve callback then finds everything pre-cached during instantiation.
1218        prefetch_module_imports(tc, bridge_ctx, module, resource_name_str);
1219
1220        // Instantiate (calls resolve callback for each import — mostly cache hits now)
1221        let inst_result = module.instantiate_module(tc, module_resolve_callback);
1222        if inst_result.is_none() {
1223            clear_module_state();
1224            return match tc.exception() {
1225                Some(e) => {
1226                    let (c, err) = exception_to_result(tc, e);
1227                    (c, None, Some(err))
1228                }
1229                None => (1, None, None),
1230            };
1231        }
1232
1233        // Evaluate
1234        let eval_result = module.evaluate(tc);
1235        if eval_result.is_none() {
1236            clear_module_state();
1237            return match tc.exception() {
1238                Some(e) => {
1239                    let (c, err) = exception_to_result(tc, e);
1240                    (c, None, Some(err))
1241                }
1242                None => (1, None, None),
1243            };
1244        }
1245
1246        // Always flush microtasks after module evaluation so that async
1247        // operations started during evaluation (e.g. process.stdin listeners,
1248        // timers) can create their pending bridge promises.  Without this,
1249        // modules without top-level await exit immediately because the session
1250        // event loop sees no pending work.
1251        if eval_result.unwrap().is_promise() {
1252            let promise = v8::Local::<v8::Promise>::try_from(eval_result.unwrap()).unwrap();
1253            tc.perform_microtask_checkpoint();
1254
1255            if let Some(exception) = tc.exception() {
1256                clear_module_state();
1257                let (c, err) = exception_to_result(tc, exception);
1258                return (c, None, Some(err));
1259            }
1260
1261            if let Some(err) = take_unhandled_promise_rejection(tc) {
1262                clear_module_state();
1263                return (1, None, Some(err));
1264            }
1265
1266            match promise.state() {
1267                v8::PromiseState::Pending => {
1268                    set_pending_module_evaluation(tc, module, promise);
1269                    return (0, None, None);
1270                }
1271                v8::PromiseState::Rejected => {
1272                    let rejection = promise.result(tc);
1273                    clear_module_state();
1274                    let (exit_code, err) = exception_to_result(tc, rejection);
1275                    return (exit_code, None, Some(err));
1276                }
1277                v8::PromiseState::Fulfilled => {}
1278            }
1279        } else {
1280            // Non-TLA module: still flush microtasks so bridge-initiated
1281            // async work (stdin reads, handle registration) becomes visible
1282            // to the session event loop.
1283            tc.perform_microtask_checkpoint();
1284
1285            if let Some(exception) = tc.exception() {
1286                clear_module_state();
1287                let (c, err) = exception_to_result(tc, exception);
1288                return (c, None, Some(err));
1289            }
1290
1291            if let Some(err) = take_unhandled_promise_rejection(tc) {
1292                clear_module_state();
1293                return (1, None, Some(err));
1294            }
1295        }
1296
1297        // Check module status for errors (handles TLA rejection case)
1298        if module.get_status() == v8::ModuleStatus::Errored {
1299            let exc = module.get_exception();
1300            clear_module_state();
1301            let (exit_code, err) = exception_to_result(tc, exc);
1302            return (exit_code, None, Some(err));
1303        }
1304
1305        let exports_bytes = match serialize_module_exports(tc, module) {
1306            Ok(bytes) => bytes,
1307            Err(err) => {
1308                clear_module_state();
1309                return (1, None, Some(err));
1310            }
1311        };
1312
1313        // Keep module resolve state available after the initial module finishes.
1314        // Dynamic imports can still fire later on the same session event loop.
1315        (0, Some(exports_bytes), None)
1316    }
1317}
1318
1319/// Extract static import specifiers from a compiled module.
1320///
1321/// Returns a list of (specifier, referrer_name) pairs for all imports
1322/// that are not already in the module cache.
1323fn extract_uncached_imports(
1324    scope: &mut v8::HandleScope,
1325    module: v8::Local<v8::Module>,
1326    referrer_name: &str,
1327) -> Vec<(String, String)> {
1328    let requests = module.get_module_requests();
1329    let mut uncached = Vec::new();
1330    for i in 0..requests.length() {
1331        if uncached.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1332            break;
1333        }
1334        let data = requests.get(scope, i).unwrap();
1335        let request: v8::Local<v8::ModuleRequest> = data.cast();
1336        let specifier = request.get_specifier().to_rust_string_lossy(scope);
1337        let cache_key = module_request_cache_key(&specifier, referrer_name);
1338
1339        // Skip if already cached for this referrer-qualified request.
1340        let already_cached = MODULE_RESOLVE_STATE.with(|cell| {
1341            let borrow = cell.borrow();
1342            let state = borrow.as_ref().unwrap();
1343            state.module_cache.contains_key(&cache_key)
1344        });
1345        if !already_cached {
1346            uncached.push((specifier, referrer_name.to_string()));
1347        }
1348    }
1349    uncached
1350}
1351
1352/// Batch-prefetch module imports via a single IPC round-trip.
1353///
1354/// Sends _batchResolveModules with all uncached specifiers, receives resolved
1355/// paths + source code, compiles and caches each module, then recurses (BFS)
1356/// for any newly discovered imports. Falls back silently if the host doesn't
1357/// support batch resolution (the resolve callback handles individual resolution).
1358fn prefetch_module_imports(
1359    scope: &mut v8::HandleScope,
1360    bridge_ctx: &BridgeCallContext,
1361    root_module: v8::Local<v8::Module>,
1362    root_name: &str,
1363) {
1364    // BFS queue: modules whose imports we need to prefetch
1365    let mut pending: Vec<(v8::Global<v8::Module>, String)> =
1366        vec![(v8::Global::new(scope, root_module), root_name.to_string())];
1367    let mut visited_modules = 0usize;
1368
1369    while !pending.is_empty() && visited_modules < MAX_MODULE_PREFETCH_GRAPH_MODULES {
1370        let remaining_modules = MAX_MODULE_PREFETCH_GRAPH_MODULES - visited_modules;
1371        let current_len = pending.len().min(remaining_modules);
1372        let current: Vec<_> = pending.drain(..current_len).collect();
1373        visited_modules += current.len();
1374
1375        // Collect all uncached imports from pending modules
1376        let mut batch: Vec<(String, String)> = Vec::new();
1377        for (global_mod, referrer) in &current {
1378            let local_mod = v8::Local::new(scope, global_mod);
1379            let imports = extract_uncached_imports(scope, local_mod, referrer);
1380            for (spec, ref_name) in imports {
1381                if batch.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1382                    break;
1383                }
1384                // Deduplicate within this batch by the full request identity.
1385                if !batch.iter().any(|(s, r)| s == &spec && r == &ref_name) {
1386                    batch.push((spec, ref_name));
1387                }
1388            }
1389            if batch.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1390                break;
1391            }
1392        }
1393
1394        if batch.is_empty() {
1395            break;
1396        }
1397
1398        // Send batch resolve+load via IPC
1399        let results = match batch_resolve_via_ipc(scope, bridge_ctx, &batch) {
1400            Some(r) => r,
1401            None => break, // Host doesn't support batch or IPC error — fall back to individual
1402        };
1403
1404        // Compile and cache each result, collect newly compiled modules for next BFS level
1405        let mut next_pending: Vec<(v8::Global<v8::Module>, String)> = Vec::new();
1406        for (i, result) in results.iter().enumerate() {
1407            if i >= batch.len() {
1408                break;
1409            }
1410            if let Some((resolved_path, source_code)) = result {
1411                // Check cache again (another entry in this batch may have resolved the same path)
1412                let already_cached = MODULE_RESOLVE_STATE.with(|cell| {
1413                    let borrow = cell.borrow();
1414                    let state = borrow.as_ref().unwrap();
1415                    state.module_cache.contains_key(resolved_path)
1416                });
1417                if already_cached {
1418                    continue;
1419                }
1420
1421                let module_format = lookup_module_format_via_ipc(scope, bridge_ctx, resolved_path);
1422                let effective_source =
1423                    build_module_source(scope, source_code, resolved_path, module_format);
1424
1425                // Compile the module
1426                let resource = match v8::String::new(scope, resolved_path) {
1427                    Some(s) => s,
1428                    None => continue,
1429                };
1430                let origin = v8::ScriptOrigin::new(
1431                    scope,
1432                    resource.into(),
1433                    0,
1434                    0,
1435                    false,
1436                    -1,
1437                    None,
1438                    false,
1439                    false,
1440                    true, // is_module
1441                    None,
1442                );
1443                let v8_source = match v8::String::new(scope, &effective_source) {
1444                    Some(s) => s,
1445                    None => continue,
1446                };
1447                let mut compiled = v8::script_compiler::Source::new(v8_source, Some(&origin));
1448                let module = match v8::script_compiler::compile_module(scope, &mut compiled) {
1449                    Some(m) => m,
1450                    None => continue,
1451                };
1452
1453                // Cache the module
1454                let global = v8::Global::new(scope, module);
1455                if !cache_resolved_module(
1456                    module,
1457                    global,
1458                    resolved_path.clone(),
1459                    Some(module_request_cache_key(&batch[i].0, &batch[i].1)),
1460                ) {
1461                    return;
1462                }
1463
1464                if visited_modules + next_pending.len() < MAX_MODULE_PREFETCH_GRAPH_MODULES {
1465                    next_pending.push((v8::Global::new(scope, module), resolved_path.clone()));
1466                }
1467            }
1468        }
1469
1470        pending = next_pending;
1471    }
1472}
1473
1474fn resolve_or_compile_module<'s>(
1475    scope: &mut v8::HandleScope<'s>,
1476    specifier_str: &str,
1477    referrer_name: &str,
1478) -> Option<v8::Local<'s, v8::Module>> {
1479    let request_cache_key = module_request_cache_key(specifier_str, referrer_name);
1480
1481    // Phase 1: Check cache by referrer-qualified request.
1482    let cached_global = MODULE_RESOLVE_STATE.with(|cell| {
1483        let borrow = cell.borrow();
1484        let state = borrow.as_ref()?;
1485        state.module_cache.get(&request_cache_key).cloned()
1486    });
1487    if let Some(cached) = cached_global {
1488        return Some(v8::Local::new(scope, &cached));
1489    }
1490
1491    // Phase 2: Get bridge context.
1492    let bridge_ctx_ptr = MODULE_RESOLVE_STATE.with(|cell| {
1493        let borrow = cell.borrow();
1494        borrow.as_ref().map(|state| state.bridge_ctx)
1495    });
1496    let bridge_ctx_ptr = bridge_ctx_ptr?;
1497    let ctx = unsafe { &*bridge_ctx_ptr };
1498
1499    // Phase 3: Resolve module path — directly on this thread via the session
1500    // reader when present (skips the bridge `_resolveModule` round-trip), else IPC.
1501    let trace = mod_trace_enabled();
1502    let t = trace.then(Instant::now);
1503    let direct_resolved = MODULE_RESOLVE_STATE.with(|cell| {
1504        cell.borrow_mut()
1505            .as_mut()
1506            .and_then(|state| state.guest_reader.as_mut())
1507            .and_then(|reader| reader.resolve_module(specifier_str, referrer_name))
1508    });
1509    let resolved_path = match direct_resolved {
1510        Some(path) => path,
1511        None => resolve_module_via_ipc(scope, ctx, specifier_str, referrer_name)?,
1512    };
1513    if let Some(t) = t {
1514        record_mod(1, t.elapsed().as_nanos() as u64);
1515    }
1516
1517    // Phase 4: Check cache by resolved path.
1518    let cached_global = MODULE_RESOLVE_STATE.with(|cell| {
1519        let borrow = cell.borrow();
1520        let state = borrow.as_ref()?;
1521        state.module_cache.get(&resolved_path).cloned()
1522    });
1523    if let Some(cached) = cached_global {
1524        return Some(v8::Local::new(scope, &cached));
1525    }
1526
1527    // Phase 5: Load the module source — directly via the session-thread reader
1528    // when present (skips the ~139us bridge round-trip), else via the bridge IPC.
1529    // guest_reader is None until the higher crate plumbs the reader down, so this
1530    // is currently a no-op fall-through to the IPC path (zero behavior change).
1531    let t = trace.then(Instant::now);
1532    let direct_source = MODULE_RESOLVE_STATE.with(|cell| {
1533        cell.borrow_mut()
1534            .as_mut()
1535            .and_then(|state| state.guest_reader.as_mut())
1536            .and_then(|reader| reader.read_module_source(&resolved_path))
1537    });
1538    let raw_source = match direct_source {
1539        Some(source) => source,
1540        None => load_module_via_ipc(scope, ctx, &resolved_path)?,
1541    };
1542    if let Some(t) = t {
1543        record_mod(2, t.elapsed().as_nanos() as u64);
1544    }
1545    let t = trace.then(Instant::now);
1546    let module_format = lookup_module_format_via_ipc(scope, ctx, &resolved_path);
1547    if let Some(t) = t {
1548        record_mod(3, t.elapsed().as_nanos() as u64);
1549    }
1550    let source_code = build_module_source(scope, &raw_source, &resolved_path, module_format);
1551
1552    let resource = v8::String::new(scope, &resolved_path)?;
1553    let origin = v8::ScriptOrigin::new(
1554        scope,
1555        resource.into(),
1556        0,
1557        0,
1558        false,
1559        -1,
1560        None,
1561        false,
1562        false,
1563        true,
1564        None,
1565    );
1566    let v8_source = match v8::String::new(scope, &source_code) {
1567        Some(s) => s,
1568        None => {
1569            throw_module_error(scope, "module source too large for V8");
1570            return None;
1571        }
1572    };
1573    let mut compiled = v8::script_compiler::Source::new(v8_source, Some(&origin));
1574    let t = trace.then(Instant::now);
1575    let module = v8::script_compiler::compile_module(scope, &mut compiled)?;
1576    if let Some(t) = t {
1577        record_mod(4, t.elapsed().as_nanos() as u64);
1578    }
1579    let global = v8::Global::new(scope, module);
1580    if !cache_resolved_module(module, global, resolved_path, Some(request_cache_key)) {
1581        throw_module_error(scope, "module resolution cache limit exceeded");
1582        return None;
1583    }
1584
1585    Some(module)
1586}
1587
1588fn cache_resolved_module(
1589    module: v8::Local<v8::Module>,
1590    global: v8::Global<v8::Module>,
1591    resolved_path: String,
1592    request_cache_key: Option<String>,
1593) -> bool {
1594    MODULE_RESOLVE_STATE.with(|cell| {
1595        let mut borrow = cell.borrow_mut();
1596        let Some(state) = borrow.as_mut() else {
1597            return true;
1598        };
1599
1600        let identity_hash = module.get_identity_hash();
1601        let new_module_name = !state.module_names.contains_key(&identity_hash);
1602        let new_resolved_path = !state.module_cache.contains_key(&resolved_path);
1603        let new_request_key = request_cache_key
1604            .as_ref()
1605            .is_some_and(|key| !state.module_cache.contains_key(key));
1606
1607        let next_module_count = state.module_names.len() + usize::from(new_module_name);
1608        let next_cache_count = state.module_cache.len()
1609            + usize::from(new_resolved_path)
1610            + usize::from(new_request_key);
1611        if next_module_count > MAX_MODULE_RESOLVE_MODULES
1612            || next_cache_count > MAX_MODULE_RESOLVE_CACHE_ENTRIES
1613        {
1614            return false;
1615        }
1616
1617        state
1618            .module_names
1619            .insert(identity_hash, resolved_path.clone());
1620        state
1621            .module_cache
1622            .insert(resolved_path.clone(), global.clone());
1623        if let Some(request_cache_key) = request_cache_key {
1624            state.module_cache.insert(request_cache_key, global);
1625        }
1626        true
1627    })
1628}
1629
1630/// Callback invoked by V8 when `import.meta` is accessed in an ES module.
1631/// Sets `import.meta.url` to a `file://` URL derived from the module's resource name.
1632#[cfg_attr(test, allow(dead_code))]
1633pub extern "C" fn import_meta_object_callback(
1634    context: v8::Local<v8::Context>,
1635    module: v8::Local<v8::Module>,
1636    meta: v8::Local<v8::Object>,
1637) {
1638    let scope = &mut unsafe { v8::CallbackScope::new(context) };
1639
1640    // Look up the module's resource name from MODULE_RESOLVE_STATE.module_names
1641    // which maps identity_hash → resource_name.
1642    let identity_hash = module.get_identity_hash();
1643    let url_str = MODULE_RESOLVE_STATE.with(|cell| {
1644        let state_opt = cell.borrow();
1645        if let Some(ref state) = *state_opt {
1646            if let Some(name) = state.module_names.get(&identity_hash) {
1647                let n = name.clone();
1648                if n.starts_with("file://") {
1649                    return Some(n);
1650                } else if n.starts_with("/") {
1651                    return Some(format!("file://{}", n));
1652                } else {
1653                    return Some(n);
1654                }
1655            }
1656        }
1657        None
1658    });
1659
1660    if let Some(url) = url_str {
1661        let key = v8::String::new(scope, "url").unwrap();
1662        let value = v8::String::new(scope, &url).unwrap();
1663        meta.set(scope, key.into(), value.into());
1664    }
1665}
1666
1667#[cfg_attr(test, allow(dead_code))]
1668fn dynamic_import_namespace_callback(
1669    _scope: &mut v8::HandleScope,
1670    args: v8::FunctionCallbackArguments,
1671    mut rv: v8::ReturnValue,
1672) {
1673    rv.set(args.data());
1674}
1675
1676#[cfg_attr(test, allow(dead_code))]
1677fn dynamic_import_reject_callback(
1678    scope: &mut v8::HandleScope,
1679    args: v8::FunctionCallbackArguments,
1680    mut rv: v8::ReturnValue,
1681) {
1682    let reason = args.get(0);
1683    scope.throw_exception(reason);
1684    rv.set(reason);
1685}
1686
1687#[cfg_attr(test, allow(dead_code))]
1688pub fn dynamic_import_callback<'a>(
1689    scope: &mut v8::HandleScope<'a>,
1690    _host_defined_options: v8::Local<'a, v8::Data>,
1691    resource_name: v8::Local<'a, v8::Value>,
1692    specifier: v8::Local<'a, v8::String>,
1693    _import_attributes: v8::Local<'a, v8::FixedArray>,
1694) -> Option<v8::Local<'a, v8::Promise>> {
1695    let tc = &mut v8::TryCatch::new(scope);
1696
1697    let specifier_str = specifier.to_rust_string_lossy(tc);
1698    let referrer_name = resolve_dynamic_import_referrer_name(tc, resource_name);
1699    let module = match resolve_or_compile_module(tc, &specifier_str, &referrer_name) {
1700        Some(module) => module,
1701        None => {
1702            let reason = if let Some(exception) = tc.exception() {
1703                exception
1704            } else {
1705                let msg = v8::String::new(tc, "Cannot dynamically import module").unwrap();
1706                v8::Exception::error(tc, msg)
1707            };
1708            return rejected_promise(tc, reason);
1709        }
1710    };
1711
1712    if module.get_status() == v8::ModuleStatus::Uninstantiated
1713        && module
1714            .instantiate_module(tc, module_resolve_callback)
1715            .is_none()
1716    {
1717        let reason = if let Some(exception) = tc.exception() {
1718            exception
1719        } else {
1720            let msg =
1721                v8::String::new(tc, "Cannot instantiate dynamically imported module").unwrap();
1722            v8::Exception::error(tc, msg)
1723        };
1724        return rejected_promise(tc, reason);
1725    }
1726
1727    if module.get_status() == v8::ModuleStatus::Errored {
1728        let exception = v8::Global::new(tc, module.get_exception());
1729        let exception = v8::Local::new(tc, &exception);
1730        return rejected_promise(tc, exception);
1731    }
1732
1733    if module.get_status() == v8::ModuleStatus::Evaluated {
1734        let namespace = v8::Global::new(tc, module.get_module_namespace());
1735        let namespace = v8::Local::new(tc, &namespace);
1736        return resolved_promise(tc, namespace);
1737    }
1738
1739    let eval_result = match module.evaluate(tc) {
1740        Some(result) => result,
1741        None => {
1742            let reason = if let Some(exception) = tc.exception() {
1743                exception
1744            } else {
1745                let msg =
1746                    v8::String::new(tc, "Cannot evaluate dynamically imported module").unwrap();
1747                v8::Exception::error(tc, msg)
1748            };
1749            return rejected_promise(tc, reason);
1750        }
1751    };
1752
1753    let namespace = v8::Global::new(tc, module.get_module_namespace());
1754    let namespace = v8::Local::new(tc, &namespace);
1755    if eval_result.is_promise() {
1756        let eval_promise = v8::Local::<v8::Promise>::try_from(eval_result).ok()?;
1757        let on_fulfilled = v8::FunctionTemplate::builder(dynamic_import_namespace_callback)
1758            .data(namespace)
1759            .build(tc)
1760            .get_function(tc)?;
1761        let on_rejected = v8::FunctionTemplate::builder(dynamic_import_reject_callback)
1762            .build(tc)
1763            .get_function(tc)?;
1764        return eval_promise.then2(tc, on_fulfilled, on_rejected);
1765    }
1766
1767    resolved_promise(tc, namespace)
1768}
1769
1770fn resolve_dynamic_import_referrer_name(
1771    scope: &mut v8::HandleScope,
1772    resource_name: v8::Local<v8::Value>,
1773) -> String {
1774    let candidate = resource_name.to_rust_string_lossy(scope);
1775    if candidate.starts_with('/') || candidate.starts_with("file://") {
1776        return candidate;
1777    }
1778
1779    let context = scope.get_current_context();
1780    let global = context.global(scope);
1781    let key = match v8::String::new(scope, "_currentModule") {
1782        Some(key) => key,
1783        None => return candidate,
1784    };
1785    let current_module = match global.get(scope, key.into()) {
1786        Some(value) if value.is_object() => value,
1787        _ => return candidate,
1788    };
1789    let current_module = match v8::Local::<v8::Object>::try_from(current_module) {
1790        Ok(object) => object,
1791        Err(_) => return candidate,
1792    };
1793    let filename_key = match v8::String::new(scope, "filename") {
1794        Some(key) => key,
1795        None => return candidate,
1796    };
1797    match current_module.get(scope, filename_key.into()) {
1798        Some(value) if value.is_string() => value.to_rust_string_lossy(scope),
1799        _ => candidate,
1800    }
1801}
1802
1803#[cfg_attr(test, allow(dead_code))]
1804fn resolved_promise<'s>(
1805    scope: &mut v8::HandleScope<'s>,
1806    value: v8::Local<'s, v8::Value>,
1807) -> Option<v8::Local<'s, v8::Promise>> {
1808    let resolver = v8::PromiseResolver::new(scope)?;
1809    resolver.resolve(scope, value);
1810    Some(resolver.get_promise(scope))
1811}
1812
1813#[cfg_attr(test, allow(dead_code))]
1814fn rejected_promise<'s>(
1815    scope: &mut v8::HandleScope<'s>,
1816    reason: v8::Local<'s, v8::Value>,
1817) -> Option<v8::Local<'s, v8::Promise>> {
1818    let resolver = v8::PromiseResolver::new(scope)?;
1819    resolver.reject(scope, reason);
1820    Some(resolver.get_promise(scope))
1821}
1822
1823/// Send _batchResolveModules via sync-blocking IPC.
1824///
1825/// Sends an array of {specifier, referrer} pairs, receives an array of
1826/// {resolved, source} results (null entries for unresolvable modules).
1827/// Returns None if the host doesn't support batch resolution or on IPC error.
1828fn batch_resolve_via_ipc(
1829    scope: &mut v8::HandleScope,
1830    ctx: &BridgeCallContext,
1831    batch: &[(String, String)],
1832) -> Option<Vec<Option<(String, String)>>> {
1833    // Build V8 array of [specifier, referrer] pairs, wrapped in an outer array
1834    // so the host handler receives the batch as a single argument (args are spread).
1835    let inner = v8::Array::new(scope, batch.len() as i32);
1836    for (i, (specifier, referrer)) in batch.iter().enumerate() {
1837        let pair = v8::Array::new(scope, 2);
1838        let spec_v8 = v8::String::new(scope, specifier)?;
1839        let ref_v8 = v8::String::new(scope, referrer)?;
1840        pair.set_index(scope, 0, spec_v8.into());
1841        pair.set_index(scope, 1, ref_v8.into());
1842        inner.set_index(scope, i as u32, pair.into());
1843    }
1844    let outer = v8::Array::new(scope, 1);
1845    outer.set_index(scope, 0, inner.into());
1846    let args = serialize_v8_value(scope, outer.into()).ok()?;
1847
1848    let response = ctx.sync_call("_batchResolveModules", args).ok()??;
1849    if response.len() > MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES {
1850        return None;
1851    }
1852    let val = deserialize_v8_value(scope, &response).ok()?;
1853
1854    // Parse response: array of {resolved, source} or null
1855    let result_arr = v8::Local::<v8::Array>::try_from(val).ok()?;
1856    let mut results = Vec::with_capacity(batch.len());
1857    for i in 0..result_arr.length().min(batch.len() as u32) {
1858        let entry = result_arr.get_index(scope, i);
1859        match entry {
1860            Some(v) if !v.is_null() && !v.is_undefined() => {
1861                let obj = v8::Local::<v8::Object>::try_from(v).ok();
1862                if let Some(obj) = obj {
1863                    let r_key = v8::String::new(scope, "resolved").unwrap();
1864                    let s_key = v8::String::new(scope, "source").unwrap();
1865                    let resolved = obj
1866                        .get(scope, r_key.into())
1867                        .filter(|v| v.is_string())
1868                        .map(|v| v.to_rust_string_lossy(scope));
1869                    let source = obj
1870                        .get(scope, s_key.into())
1871                        .filter(|v| v.is_string())
1872                        .map(|v| v.to_rust_string_lossy(scope));
1873                    match (resolved, source) {
1874                        (Some(r), Some(s)) => results.push(Some((r, s))),
1875                        _ => results.push(None),
1876                    }
1877                } else {
1878                    results.push(None);
1879                }
1880            }
1881            _ => results.push(None),
1882        }
1883    }
1884    Some(results)
1885}
1886
1887/// V8 ResolveModuleCallback — called during instantiate_module for each import.
1888///
1889/// Sends sync-blocking IPC calls to resolve specifiers and load source code,
1890/// compiles resolved modules, and caches them.
1891fn module_resolve_callback<'a>(
1892    context: v8::Local<'a, v8::Context>,
1893    specifier: v8::Local<'a, v8::String>,
1894    _import_attributes: v8::Local<'a, v8::FixedArray>,
1895    referrer: v8::Local<'a, v8::Module>,
1896) -> Option<v8::Local<'a, v8::Module>> {
1897    // SAFETY: CallbackScope can be constructed from Local<Context> within a V8 callback
1898    let scope = &mut unsafe { v8::CallbackScope::new(context) };
1899
1900    let specifier_str = specifier.to_rust_string_lossy(scope);
1901    let referrer_hash = referrer.get_identity_hash();
1902
1903    let referrer_name = MODULE_RESOLVE_STATE.with(|cell| {
1904        let borrow = cell.borrow();
1905        let state = borrow.as_ref()?;
1906        state.module_names.get(&referrer_hash).cloned()
1907    });
1908    let referrer_name = referrer_name?;
1909    resolve_or_compile_module(scope, &specifier_str, &referrer_name)
1910}
1911
1912/// Send _resolveModule(specifier, referrer_path) via sync-blocking IPC.
1913fn resolve_module_via_ipc(
1914    scope: &mut v8::HandleScope,
1915    ctx: &BridgeCallContext,
1916    specifier: &str,
1917    referrer: &str,
1918) -> Option<String> {
1919    // Serialize [specifier, referrer] as V8 Array
1920    let spec_v8 = v8::String::new(scope, specifier).unwrap();
1921    let ref_v8 = v8::String::new(scope, referrer).unwrap();
1922    let arr = v8::Array::new(scope, 2);
1923    arr.set_index(scope, 0, spec_v8.into());
1924    arr.set_index(scope, 1, ref_v8.into());
1925    let args = match serialize_v8_value(scope, arr.into()) {
1926        Ok(bytes) => bytes,
1927        Err(e) => {
1928            throw_module_error(scope, &format!("_resolveModule serialize error: {}", e));
1929            return None;
1930        }
1931    };
1932
1933    match ctx.sync_call("_resolveModule", args) {
1934        Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
1935            Ok(val) => {
1936                if val.is_string() {
1937                    Some(val.to_rust_string_lossy(scope))
1938                } else {
1939                    // A non-string (null) return means the host resolver found no
1940                    // match — i.e. the module could not be located, NOT a type error.
1941                    // Name the importer so node_modules layout/discovery problems are
1942                    // diagnosable (e.g. a bare package installed off the importer's
1943                    // ancestor chain), since that is the common real cause here.
1944                    //
1945                    // Call out the host-mounted node_modules case too: a host_dir
1946                    // mount (what NodeRuntime `nodeModules` projects) confines reads
1947                    // to the mount root, so a package symlinked OUT of the mounted
1948                    // tree (pnpm/yarn workspace or `file:` deps that link to the
1949                    // workspace root or an external store) cannot be followed and
1950                    // surfaces here as not-found.
1951                    throw_module_error(
1952                        scope,
1953                        &format!(
1954                            "Cannot resolve module '{specifier}' (imported from \
1955                             '{referrer}'): not found. For a bare package, ensure it is \
1956                             installed in a node_modules directory on an ancestor of the \
1957                             importer (or bundle the entrypoint). If you mounted a host \
1958                             node_modules, point it at a directory that contains every \
1959                             symlink target (e.g. the workspace root): symlinks that \
1960                             escape the mount root are not followed."
1961                        ),
1962                    );
1963                    None
1964                }
1965            }
1966            Err(e) => {
1967                throw_module_error(scope, &format!("_resolveModule decode error: {}", e));
1968                None
1969            }
1970        },
1971        Ok(None) => {
1972            throw_module_error(scope, &format!("Cannot resolve module '{}'", specifier));
1973            None
1974        }
1975        Err(e) => {
1976            throw_module_error(scope, &e);
1977            None
1978        }
1979    }
1980}
1981
1982/// Send _loadFile(resolved_path) via sync-blocking IPC.
1983fn load_module_via_ipc(
1984    scope: &mut v8::HandleScope,
1985    ctx: &BridgeCallContext,
1986    resolved_path: &str,
1987) -> Option<String> {
1988    // Serialize [resolved_path] as V8 Array
1989    let path_v8 = v8::String::new(scope, resolved_path).unwrap();
1990    let arr = v8::Array::new(scope, 1);
1991    arr.set_index(scope, 0, path_v8.into());
1992    let args = match serialize_v8_value(scope, arr.into()) {
1993        Ok(bytes) => bytes,
1994        Err(e) => {
1995            throw_module_error(scope, &format!("_loadFile serialize error: {}", e));
1996            return None;
1997        }
1998    };
1999
2000    let ipc_result = ctx.sync_call("_loadFile", args);
2001    match ipc_result {
2002        Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
2003            Ok(val) => {
2004                if val.is_string() {
2005                    Some(val.to_rust_string_lossy(scope))
2006                } else {
2007                    throw_module_error(
2008                        scope,
2009                        &format!("_loadFile returned non-string for '{}'", resolved_path),
2010                    );
2011                    None
2012                }
2013            }
2014            Err(e) => {
2015                throw_module_error(scope, &format!("_loadFile decode error: {}", e));
2016                None
2017            }
2018        },
2019        Ok(None) => {
2020            throw_module_error(scope, &format!("Cannot load module '{}'", resolved_path));
2021            None
2022        }
2023        Err(e) => {
2024            throw_module_error(scope, &e);
2025            None
2026        }
2027    }
2028}
2029
2030#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2031enum ResolvedModuleFormat {
2032    Module,
2033    Commonjs,
2034    Json,
2035}
2036
2037fn lookup_module_format_via_ipc(
2038    scope: &mut v8::HandleScope,
2039    ctx: &BridgeCallContext,
2040    resolved_path: &str,
2041) -> Option<ResolvedModuleFormat> {
2042    let path_v8 = v8::String::new(scope, resolved_path).unwrap();
2043    let arr = v8::Array::new(scope, 1);
2044    arr.set_index(scope, 0, path_v8.into());
2045    let args = match serialize_v8_value(scope, arr.into()) {
2046        Ok(bytes) => bytes,
2047        Err(e) => {
2048            throw_module_error(scope, &format!("_moduleFormat serialize error: {}", e));
2049            return None;
2050        }
2051    };
2052
2053    match ctx.sync_call("_moduleFormat", args) {
2054        Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
2055            Ok(val) if val.is_string() => match val.to_rust_string_lossy(scope).as_str() {
2056                "module" => Some(ResolvedModuleFormat::Module),
2057                "commonjs" => Some(ResolvedModuleFormat::Commonjs),
2058                "json" => Some(ResolvedModuleFormat::Json),
2059                _ => None,
2060            },
2061            Ok(val) if val.is_null_or_undefined() => None,
2062            Ok(_) => {
2063                throw_module_error(
2064                    scope,
2065                    &format!("_moduleFormat returned non-string for '{}'", resolved_path),
2066                );
2067                None
2068            }
2069            Err(e) => {
2070                throw_module_error(scope, &format!("_moduleFormat decode error: {}", e));
2071                None
2072            }
2073        },
2074        Ok(None) => None,
2075        Err(e) => {
2076            throw_module_error(scope, &e);
2077            None
2078        }
2079    }
2080}
2081
2082/// Throw a V8 exception for module resolution errors.
2083fn throw_module_error(scope: &mut v8::HandleScope, message: &str) {
2084    let msg = v8::String::new(scope, message).unwrap();
2085    let exc = v8::Exception::error(scope, msg);
2086    scope.throw_exception(exc);
2087}
2088
2089/// Detect if source code is likely CommonJS (not ESM).
2090/// Checks for module.exports, exports.X, or require() patterns without ESM import/export.
2091/// Node strips a leading shebang (`#!`) line before parsing a module. The guest
2092/// loader must match, or modules shipped as executables (CLI/SDK bundles that
2093/// begin with `#!/usr/bin/env node`) fail with "Invalid or unexpected token" on
2094/// the `#`. The newline is preserved so line numbers in stack traces stay aligned.
2095fn strip_leading_shebang(source: &str) -> &str {
2096    match source.strip_prefix("#!") {
2097        Some(rest) => match rest.find('\n') {
2098            Some(idx) => &rest[idx..],
2099            None => "",
2100        },
2101        None => source,
2102    }
2103}
2104
2105fn build_module_source(
2106    scope: &mut v8::HandleScope,
2107    raw_source: &str,
2108    resolved_path: &str,
2109    module_format: Option<ResolvedModuleFormat>,
2110) -> String {
2111    let raw_source = strip_leading_shebang(raw_source);
2112    let normalized_path = resolved_path.to_ascii_lowercase();
2113    if normalized_path.ends_with(".json") || module_format == Some(ResolvedModuleFormat::Json) {
2114        return build_json_esm_shim(resolved_path);
2115    }
2116    if (module_format == Some(ResolvedModuleFormat::Commonjs)
2117        && !has_probable_esm_syntax(raw_source))
2118        || is_likely_cjs(raw_source, resolved_path, module_format)
2119    {
2120        return build_cjs_esm_shim(scope, raw_source, resolved_path);
2121    }
2122    add_esm_runtime_prelude(raw_source)
2123}
2124
2125fn build_json_esm_shim(resolved_path: &str) -> String {
2126    format!(
2127        "const _jsonModule = globalThis._requireFrom({}, \"/\");\nexport default _jsonModule;\n",
2128        quoted_module_path(resolved_path)
2129    )
2130}
2131
2132fn build_cjs_esm_shim(
2133    scope: &mut v8::HandleScope,
2134    raw_source: &str,
2135    resolved_path: &str,
2136) -> String {
2137    // Static scanning only sees exports assigned with literal `exports.X =` /
2138    // `Object.defineProperty(exports, "X", ...)` patterns in this file. It misses names introduced at
2139    // runtime, e.g. tsc's `__exportStar(require("./sub"), exports)` re-export helper (used by
2140    // `@sinclair/typebox/compiler` to surface `TypeCompiler`) or `Object.assign(exports, ...)`. When
2141    // such a dynamic re-export pattern is present the static set is provably incomplete, so fall back
2142    // to runtime extraction (require the module and enumerate the real `Object.keys(module.exports)`)
2143    // and union the two. Only do this when static finds nothing or a dynamic re-export is detected:
2144    // eagerly requiring every CJS module would add avoidable work and trigger side effects earlier
2145    // than intended (see crates/execution/CLAUDE.md). Static still back-fills names that a
2146    // partially-evaluated circular require may not have added to the exports object yet.
2147    let mut names = extract_cjs_export_names(raw_source)
2148        .into_iter()
2149        .collect::<HashSet<_>>();
2150    if names.is_empty() || source_has_dynamic_cjs_reexports(raw_source) {
2151        names.extend(extract_runtime_cjs_export_names(scope, resolved_path));
2152    }
2153
2154    let mut exports = names.into_iter().collect::<Vec<_>>();
2155    exports.sort();
2156    exports.truncate(MAX_CJS_NAMED_EXPORTS);
2157
2158    let mut shim = format!(
2159        "const _cjsModule = globalThis._requireFrom({}, \"/\");\nexport default _cjsModule;\n",
2160        quoted_module_path(resolved_path)
2161    );
2162    for name in exports {
2163        shim.push_str(&format!(
2164            "export const {} = _cjsModule[\"{}\"];\n",
2165            name, name
2166        ));
2167    }
2168    shim
2169}
2170
2171/// Runtime fallback for CJS named export extraction. Evaluates the module via
2172/// `globalThis._requireFrom` and enumerates `Object.keys(module.exports)` so
2173/// dynamically computed exports still support named ESM imports. A thread-local
2174/// in-progress set guards against pathological reentrancy: if shim construction
2175/// for a path somehow re-enters extraction for the same path, the inner call
2176/// returns an empty list instead of recursing.
2177fn extract_runtime_cjs_export_names(
2178    scope: &mut v8::HandleScope,
2179    resolved_path: &str,
2180) -> Vec<String> {
2181    let already_in_progress = CJS_RUNTIME_EXTRACTION_IN_PROGRESS.with(|cell| {
2182        let mut in_progress = cell.borrow_mut();
2183        !in_progress.insert(resolved_path.to_string())
2184    });
2185    if already_in_progress {
2186        return Vec::new();
2187    }
2188    let names = extract_runtime_cjs_export_names_inner(scope, resolved_path);
2189    CJS_RUNTIME_EXTRACTION_IN_PROGRESS.with(|cell| {
2190        cell.borrow_mut().remove(resolved_path);
2191    });
2192    names
2193}
2194
2195fn extract_runtime_cjs_export_names_inner(
2196    scope: &mut v8::HandleScope,
2197    resolved_path: &str,
2198) -> Vec<String> {
2199    let tc = &mut v8::TryCatch::new(scope);
2200    let context = tc.get_current_context();
2201    let global = context.global(tc);
2202
2203    let require_key = match v8::String::new(tc, "_requireFrom") {
2204        Some(key) => key,
2205        None => return Vec::new(),
2206    };
2207    let require_fn = match global
2208        .get(tc, require_key.into())
2209        .and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
2210    {
2211        Some(function) => function,
2212        None => return Vec::new(),
2213    };
2214
2215    let module_path = match v8::String::new(tc, resolved_path) {
2216        Some(path) => path,
2217        None => return Vec::new(),
2218    };
2219    let root = match v8::String::new(tc, "/") {
2220        Some(path) => path,
2221        None => return Vec::new(),
2222    };
2223    let require_args = [module_path.into(), root.into()];
2224    let receiver = v8::undefined(tc).into();
2225    let required_module = match require_fn.call(tc, receiver, &require_args) {
2226        Some(value) => value,
2227        None => return Vec::new(),
2228    };
2229    if required_module.is_null_or_undefined() || !required_module.is_object() {
2230        return Vec::new();
2231    }
2232
2233    let object_key = match v8::String::new(tc, "Object") {
2234        Some(key) => key,
2235        None => return Vec::new(),
2236    };
2237    let object_ctor = match global
2238        .get(tc, object_key.into())
2239        .and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
2240    {
2241        Some(object) => object,
2242        None => return Vec::new(),
2243    };
2244
2245    let keys_key = match v8::String::new(tc, "keys") {
2246        Some(key) => key,
2247        None => return Vec::new(),
2248    };
2249    let keys_fn = match object_ctor
2250        .get(tc, keys_key.into())
2251        .and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
2252    {
2253        Some(function) => function,
2254        None => return Vec::new(),
2255    };
2256
2257    let keys_args = [required_module];
2258    let keys = match keys_fn
2259        .call(tc, object_ctor.into(), &keys_args)
2260        .and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
2261    {
2262        Some(array) => array,
2263        None => return Vec::new(),
2264    };
2265
2266    let mut names = Vec::new();
2267    for index in 0..keys.length() {
2268        if names.len() >= MAX_CJS_NAMED_EXPORTS {
2269            break;
2270        }
2271        let Some(value) = keys.get_index(tc, index) else {
2272            continue;
2273        };
2274        if !value.is_string() {
2275            continue;
2276        }
2277        let name = value.to_rust_string_lossy(tc);
2278        if name.len() > MAX_CJS_RUNTIME_EXPORT_NAME_LEN {
2279            continue;
2280        }
2281        if is_valid_js_ident(&name) && name != "default" && name != "__esModule" {
2282            names.push(name);
2283        }
2284    }
2285    names.sort();
2286    names.dedup();
2287    names
2288}
2289
2290fn quoted_module_path(resolved_path: &str) -> String {
2291    format!(
2292        "\"{}\"",
2293        resolved_path.replace('\\', "\\\\").replace('"', "\\\"")
2294    )
2295}
2296
2297fn is_likely_cjs(
2298    source: &str,
2299    resolved_path: &str,
2300    module_format: Option<ResolvedModuleFormat>,
2301) -> bool {
2302    let normalized_path = resolved_path.to_ascii_lowercase();
2303    if normalized_path.ends_with(".mjs") || normalized_path.ends_with(".mts") {
2304        return false;
2305    }
2306    if normalized_path.ends_with(".cjs") || normalized_path.ends_with(".cts") {
2307        return true;
2308    }
2309    if module_format == Some(ResolvedModuleFormat::Module) {
2310        return false;
2311    }
2312    if has_probable_esm_syntax(source) {
2313        return false;
2314    }
2315    // CJS indicators
2316    source.contains("module.exports") || source.contains("exports.") || source.contains("require(")
2317}
2318
2319fn has_probable_esm_syntax(source: &str) -> bool {
2320    #[derive(Clone, Copy, PartialEq, Eq)]
2321    enum ScanState {
2322        Code,
2323        LineComment,
2324        BlockComment,
2325        SingleQuote,
2326        DoubleQuote,
2327        Template,
2328    }
2329
2330    let bytes = source.as_bytes();
2331    let mut state = ScanState::Code;
2332    let mut index = 0usize;
2333    let mut brace_depth = 0u32;
2334    let mut paren_depth = 0u32;
2335    let mut bracket_depth = 0u32;
2336
2337    while index < bytes.len() {
2338        let byte = bytes[index];
2339        let next = bytes.get(index + 1).copied();
2340
2341        match state {
2342            ScanState::Code => {
2343                if index == 0 && byte == b'#' && next == Some(b'!') {
2344                    state = ScanState::LineComment;
2345                    index += 2;
2346                    continue;
2347                }
2348                if byte == b'/' && next == Some(b'/') {
2349                    state = ScanState::LineComment;
2350                    index += 2;
2351                    continue;
2352                }
2353                if byte == b'/' && next == Some(b'*') {
2354                    state = ScanState::BlockComment;
2355                    index += 2;
2356                    continue;
2357                }
2358                if byte == b'\'' {
2359                    state = ScanState::SingleQuote;
2360                    index += 1;
2361                    continue;
2362                }
2363                if byte == b'"' {
2364                    state = ScanState::DoubleQuote;
2365                    index += 1;
2366                    continue;
2367                }
2368                if byte == b'`' {
2369                    state = ScanState::Template;
2370                    index += 1;
2371                    continue;
2372                }
2373
2374                match byte {
2375                    b'{' => brace_depth = brace_depth.saturating_add(1),
2376                    b'}' => brace_depth = brace_depth.saturating_sub(1),
2377                    b'(' => paren_depth = paren_depth.saturating_add(1),
2378                    b')' => paren_depth = paren_depth.saturating_sub(1),
2379                    b'[' => bracket_depth = bracket_depth.saturating_add(1),
2380                    b']' => bracket_depth = bracket_depth.saturating_sub(1),
2381                    _ => {}
2382                }
2383
2384                if brace_depth == 0
2385                    && paren_depth == 0
2386                    && bracket_depth == 0
2387                    && is_js_ident_start(byte)
2388                {
2389                    let start = index;
2390                    index += 1;
2391                    while index < bytes.len() && is_js_ident_continue(bytes[index]) {
2392                        index += 1;
2393                    }
2394
2395                    let token = &source[start..index];
2396                    if token == "export" {
2397                        return true;
2398                    }
2399                    if token == "import" {
2400                        let mut cursor = index;
2401                        while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
2402                            cursor += 1;
2403                        }
2404                        if bytes.get(cursor).copied() != Some(b'(') {
2405                            return true;
2406                        }
2407                    }
2408
2409                    continue;
2410                }
2411
2412                index += 1;
2413            }
2414            ScanState::LineComment => {
2415                if byte == b'\n' {
2416                    state = ScanState::Code;
2417                }
2418                index += 1;
2419            }
2420            ScanState::BlockComment => {
2421                if byte == b'*' && next == Some(b'/') {
2422                    state = ScanState::Code;
2423                    index += 2;
2424                } else {
2425                    index += 1;
2426                }
2427            }
2428            ScanState::SingleQuote => {
2429                if byte == b'\\' {
2430                    index += 2;
2431                } else if byte == b'\'' {
2432                    state = ScanState::Code;
2433                    index += 1;
2434                } else {
2435                    index += 1;
2436                }
2437            }
2438            ScanState::DoubleQuote => {
2439                if byte == b'\\' {
2440                    index += 2;
2441                } else if byte == b'"' {
2442                    state = ScanState::Code;
2443                    index += 1;
2444                } else {
2445                    index += 1;
2446                }
2447            }
2448            ScanState::Template => {
2449                if byte == b'\\' {
2450                    index += 2;
2451                } else if byte == b'`' {
2452                    state = ScanState::Code;
2453                    index += 1;
2454                } else {
2455                    index += 1;
2456                }
2457            }
2458        }
2459    }
2460
2461    false
2462}
2463
2464fn is_js_ident_start(byte: u8) -> bool {
2465    byte.is_ascii_alphabetic() || byte == b'_' || byte == b'$'
2466}
2467
2468fn is_js_ident_continue(byte: u8) -> bool {
2469    is_js_ident_start(byte) || byte.is_ascii_digit()
2470}
2471
2472/// Extract named export names from CJS source by scanning for `exports.X =` and
2473/// `module.exports = { X: ... }` patterns. Returns a list of valid JS identifiers.
2474fn extract_cjs_export_names(source: &str) -> Vec<String> {
2475    let mut names = HashSet::new();
2476
2477    collect_cjs_property_assignment_names(source, &mut names);
2478    collect_cjs_define_property_names(source, &mut names);
2479    collect_cjs_object_literal_export_names(source, &mut names);
2480
2481    let mut result: Vec<String> = names.into_iter().collect();
2482    result.sort();
2483    result
2484}
2485
2486fn collect_cjs_property_assignment_names(
2487    source: &str,
2488    names: &mut std::collections::HashSet<String>,
2489) {
2490    for prefix in ["exports.", "module.exports."] {
2491        let mut cursor = 0usize;
2492        while names.len() < MAX_CJS_NAMED_EXPORTS {
2493            let Some(start) = find_code_pattern(source, prefix, cursor) else {
2494                break;
2495            };
2496            let name_start = start + prefix.len();
2497            let mut index = name_start;
2498            while source
2499                .as_bytes()
2500                .get(index)
2501                .is_some_and(|byte| is_js_ident_continue(*byte))
2502            {
2503                index += 1;
2504            }
2505            let name = &source[name_start..index];
2506            let next = skip_ascii_whitespace(source, index);
2507            if source.as_bytes().get(next) == Some(&b'=')
2508                && is_valid_js_ident(name)
2509                && name != "default"
2510                && name != "__esModule"
2511            {
2512                names.insert(name.to_string());
2513            }
2514            cursor = index.max(start + prefix.len());
2515        }
2516    }
2517}
2518
2519fn collect_cjs_define_property_names(source: &str, names: &mut std::collections::HashSet<String>) {
2520    let mut cursor = 0usize;
2521    while names.len() < MAX_CJS_NAMED_EXPORTS {
2522        let Some(start) = find_code_pattern(source, "Object.defineProperty", cursor) else {
2523            break;
2524        };
2525        let mut index = skip_ascii_whitespace(source, start + "Object.defineProperty".len());
2526        if source.as_bytes().get(index) != Some(&b'(') {
2527            cursor = start + "Object.defineProperty".len();
2528            continue;
2529        }
2530        index = skip_ascii_whitespace(source, index + 1);
2531        if !source.as_bytes()[index..].starts_with(b"exports") {
2532            cursor = start + "Object.defineProperty".len();
2533            continue;
2534        }
2535        index = skip_ascii_whitespace(source, index + "exports".len());
2536        if source.as_bytes().get(index) != Some(&b',') {
2537            cursor = start + "Object.defineProperty".len();
2538            continue;
2539        }
2540        index = skip_ascii_whitespace(source, index + 1);
2541        if let Some((name, end)) = parse_quoted_string_literal(source, index) {
2542            if is_valid_js_ident(name) && name != "default" && name != "__esModule" {
2543                names.insert(name.to_string());
2544                cursor = end;
2545                continue;
2546            }
2547        }
2548        cursor = start + "Object.defineProperty".len();
2549    }
2550}
2551
2552fn collect_cjs_object_literal_export_names(
2553    source: &str,
2554    names: &mut std::collections::HashSet<String>,
2555) {
2556    collect_module_exports_assignments(source, names);
2557    collect_object_assign_module_exports(source, names);
2558}
2559
2560fn collect_module_exports_assignments(source: &str, names: &mut std::collections::HashSet<String>) {
2561    let mut cursor = 0usize;
2562    while names.len() < MAX_CJS_NAMED_EXPORTS {
2563        let Some(start) = find_code_pattern(source, "module.exports", cursor) else {
2564            break;
2565        };
2566        let mut index = skip_ascii_whitespace(source, start + "module.exports".len());
2567        if source.as_bytes().get(index) != Some(&b'=') {
2568            cursor = start + "module.exports".len();
2569            continue;
2570        }
2571        index = skip_ascii_whitespace(source, index + 1);
2572        cursor = if source.as_bytes().get(index) == Some(&b'{') {
2573            collect_object_literal_keys(source, index, names)
2574        } else {
2575            index.saturating_add(1)
2576        };
2577    }
2578}
2579
2580fn collect_object_assign_module_exports(
2581    source: &str,
2582    names: &mut std::collections::HashSet<String>,
2583) {
2584    let mut cursor = 0usize;
2585    while names.len() < MAX_CJS_NAMED_EXPORTS {
2586        let Some(start) = find_code_pattern(source, "Object.assign", cursor) else {
2587            break;
2588        };
2589        let mut index = skip_ascii_whitespace(source, start + "Object.assign".len());
2590        if source.as_bytes().get(index) != Some(&b'(') {
2591            cursor = start + "Object.assign".len();
2592            continue;
2593        }
2594        index = skip_ascii_whitespace(source, index + 1);
2595        if !source.as_bytes()[index..].starts_with(b"module.exports") {
2596            cursor = start + "Object.assign".len();
2597            continue;
2598        }
2599        index = skip_ascii_whitespace(source, index + "module.exports".len());
2600        if source.as_bytes().get(index) != Some(&b',') {
2601            cursor = start + "Object.assign".len();
2602            continue;
2603        }
2604        index = skip_ascii_whitespace(source, index + 1);
2605        cursor = if source.as_bytes().get(index) == Some(&b'{') {
2606            collect_object_literal_keys(source, index, names)
2607        } else {
2608            index.saturating_add(1)
2609        };
2610    }
2611}
2612
2613#[derive(Clone, Copy, PartialEq, Eq)]
2614enum CjsScanState {
2615    Code,
2616    LineComment,
2617    BlockComment,
2618    SingleQuote,
2619    DoubleQuote,
2620    Template,
2621    Regex,
2622    RegexClass,
2623}
2624
2625fn find_code_pattern(source: &str, pattern: &str, cursor: usize) -> Option<usize> {
2626    let bytes = source.as_bytes();
2627    let mut state = CjsScanState::Code;
2628    let mut index = cursor;
2629    while index < bytes.len() {
2630        let byte = bytes[index];
2631        let next = bytes.get(index + 1).copied();
2632
2633        match state {
2634            CjsScanState::Code => {
2635                if byte == b'/' && next == Some(b'/') {
2636                    state = CjsScanState::LineComment;
2637                    index += 2;
2638                    continue;
2639                }
2640                if byte == b'/' && next == Some(b'*') {
2641                    state = CjsScanState::BlockComment;
2642                    index += 2;
2643                    continue;
2644                }
2645                if byte == b'\'' {
2646                    state = CjsScanState::SingleQuote;
2647                    index += 1;
2648                    continue;
2649                }
2650                if byte == b'"' {
2651                    state = CjsScanState::DoubleQuote;
2652                    index += 1;
2653                    continue;
2654                }
2655                if byte == b'`' {
2656                    state = CjsScanState::Template;
2657                    index += 1;
2658                    continue;
2659                }
2660                if byte == b'/' && slash_starts_regex_literal(source, index) {
2661                    state = CjsScanState::Regex;
2662                    index += 1;
2663                    continue;
2664                }
2665                if bytes[index..].starts_with(pattern.as_bytes())
2666                    && has_code_pattern_boundary(source, index, pattern)
2667                {
2668                    return Some(index);
2669                }
2670                index += 1;
2671            }
2672            CjsScanState::LineComment => {
2673                if byte == b'\n' {
2674                    state = CjsScanState::Code;
2675                }
2676                index += 1;
2677            }
2678            CjsScanState::BlockComment => {
2679                if byte == b'*' && next == Some(b'/') {
2680                    state = CjsScanState::Code;
2681                    index += 2;
2682                } else {
2683                    index += 1;
2684                }
2685            }
2686            CjsScanState::SingleQuote => {
2687                if byte == b'\\' {
2688                    index += 2;
2689                } else if byte == b'\'' {
2690                    state = CjsScanState::Code;
2691                    index += 1;
2692                } else {
2693                    index += 1;
2694                }
2695            }
2696            CjsScanState::DoubleQuote => {
2697                if byte == b'\\' {
2698                    index += 2;
2699                } else if byte == b'"' {
2700                    state = CjsScanState::Code;
2701                    index += 1;
2702                } else {
2703                    index += 1;
2704                }
2705            }
2706            CjsScanState::Template => {
2707                if byte == b'\\' {
2708                    index += 2;
2709                } else if byte == b'`' {
2710                    state = CjsScanState::Code;
2711                    index += 1;
2712                } else {
2713                    index += 1;
2714                }
2715            }
2716            CjsScanState::Regex => {
2717                if byte == b'\\' {
2718                    index += 2;
2719                } else if byte == b'[' {
2720                    state = CjsScanState::RegexClass;
2721                    index += 1;
2722                } else if byte == b'/' {
2723                    state = CjsScanState::Code;
2724                    index += 1;
2725                } else {
2726                    index += 1;
2727                }
2728            }
2729            CjsScanState::RegexClass => {
2730                if byte == b'\\' {
2731                    index += 2;
2732                } else if byte == b']' {
2733                    state = CjsScanState::Regex;
2734                    index += 1;
2735                } else {
2736                    index += 1;
2737                }
2738            }
2739        }
2740    }
2741    None
2742}
2743
2744fn slash_starts_regex_literal(source: &str, slash_index: usize) -> bool {
2745    let bytes = source.as_bytes();
2746    let mut cursor = slash_index;
2747    while cursor > 0 {
2748        cursor -= 1;
2749        if bytes[cursor].is_ascii_whitespace() {
2750            continue;
2751        }
2752        return match bytes[cursor] {
2753            b'(' | b')' | b'[' | b'{' | b'}' | b':' | b',' | b';' | b'=' | b'!' | b'?' | b'&'
2754            | b'|' | b'+' | b'-' | b'*' | b'%' | b'^' | b'~' | b'<' => true,
2755            b'>' => cursor > 0 && bytes[cursor - 1] == b'=',
2756            byte if is_js_ident_continue(byte) => {
2757                let end = cursor + 1;
2758                let mut start = cursor;
2759                while start > 0 && is_js_ident_continue(bytes[start - 1]) {
2760                    start -= 1;
2761                }
2762                matches!(
2763                    &source[start..end],
2764                    "await"
2765                        | "case"
2766                        | "delete"
2767                        | "do"
2768                        | "else"
2769                        | "in"
2770                        | "instanceof"
2771                        | "of"
2772                        | "return"
2773                        | "throw"
2774                        | "typeof"
2775                        | "void"
2776                        | "yield"
2777                )
2778            }
2779            _ => false,
2780        };
2781    }
2782    true
2783}
2784
2785fn has_code_pattern_boundary(source: &str, index: usize, pattern: &str) -> bool {
2786    let bytes = source.as_bytes();
2787    let before_ok = index == 0
2788        || bytes
2789            .get(index - 1)
2790            .is_none_or(|byte| !is_js_ident_continue(*byte) && *byte != b'.');
2791    let end = index + pattern.len();
2792    let after_ok = pattern.ends_with('.')
2793        || bytes
2794            .get(end)
2795            .is_none_or(|byte| !is_js_ident_continue(*byte));
2796    before_ok && after_ok
2797}
2798
2799fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
2800    while source
2801        .as_bytes()
2802        .get(index)
2803        .is_some_and(u8::is_ascii_whitespace)
2804    {
2805        index += 1;
2806    }
2807    index
2808}
2809
2810fn collect_object_literal_keys(
2811    source: &str,
2812    open_brace: usize,
2813    names: &mut std::collections::HashSet<String>,
2814) -> usize {
2815    let mut depth = 0usize;
2816    let mut state = CjsScanState::Code;
2817    let mut entry_start = open_brace + 1;
2818    let bytes = source.as_bytes();
2819    let mut iter = source[open_brace..].char_indices().peekable();
2820    while let Some((offset, ch)) = iter.next() {
2821        let index = open_brace + offset;
2822        let byte = bytes[index];
2823        let next = bytes.get(index + 1).copied();
2824
2825        match state {
2826            CjsScanState::Code => {
2827                if byte == b'/' && next == Some(b'/') {
2828                    state = CjsScanState::LineComment;
2829                    continue;
2830                }
2831                if byte == b'/' && next == Some(b'*') {
2832                    state = CjsScanState::BlockComment;
2833                    continue;
2834                }
2835                if byte == b'\'' {
2836                    state = CjsScanState::SingleQuote;
2837                    continue;
2838                }
2839                if byte == b'"' {
2840                    state = CjsScanState::DoubleQuote;
2841                    continue;
2842                }
2843                if byte == b'`' {
2844                    state = CjsScanState::Template;
2845                    continue;
2846                }
2847                if byte == b'/' && slash_starts_regex_literal(source, index) {
2848                    state = CjsScanState::Regex;
2849                    continue;
2850                }
2851                match ch {
2852                    '{' | '[' | '(' => depth += 1,
2853                    '}' | ']' | ')' => {
2854                        depth = depth.saturating_sub(1);
2855                        if depth == 0 && ch == '}' {
2856                            collect_object_literal_entry(&source[entry_start..index], names);
2857                            return index + ch.len_utf8();
2858                        }
2859                    }
2860                    ',' if depth == 1 => {
2861                        collect_object_literal_entry(&source[entry_start..index], names);
2862                        if names.len() >= MAX_CJS_NAMED_EXPORTS {
2863                            return index + ch.len_utf8();
2864                        }
2865                        entry_start = index + ch.len_utf8();
2866                    }
2867                    _ => {}
2868                }
2869            }
2870            CjsScanState::LineComment => {
2871                if byte == b'\n' {
2872                    state = CjsScanState::Code;
2873                }
2874            }
2875            CjsScanState::BlockComment => {
2876                if byte == b'*' && next == Some(b'/') {
2877                    state = CjsScanState::Code;
2878                    iter.next();
2879                }
2880            }
2881            CjsScanState::SingleQuote => {
2882                if byte == b'\\' {
2883                    iter.next();
2884                } else if byte == b'\'' {
2885                    state = CjsScanState::Code;
2886                }
2887            }
2888            CjsScanState::DoubleQuote => {
2889                if byte == b'\\' {
2890                    iter.next();
2891                } else if byte == b'"' {
2892                    state = CjsScanState::Code;
2893                }
2894            }
2895            CjsScanState::Template => {
2896                if byte == b'\\' {
2897                    iter.next();
2898                } else if byte == b'`' {
2899                    state = CjsScanState::Code;
2900                }
2901            }
2902            CjsScanState::Regex => {
2903                if byte == b'\\' {
2904                    iter.next();
2905                } else if byte == b'[' {
2906                    state = CjsScanState::RegexClass;
2907                } else if byte == b'/' {
2908                    state = CjsScanState::Code;
2909                }
2910            }
2911            CjsScanState::RegexClass => {
2912                if byte == b'\\' {
2913                    iter.next();
2914                } else if byte == b']' {
2915                    state = CjsScanState::Regex;
2916                }
2917            }
2918        }
2919    }
2920    source.len()
2921}
2922
2923fn collect_object_literal_entry(entry: &str, names: &mut std::collections::HashSet<String>) {
2924    let key = entry_key(entry);
2925    if is_valid_js_ident(key) && key != "default" && key != "__esModule" {
2926        names.insert(key.to_string());
2927    }
2928}
2929
2930fn entry_key(entry: &str) -> &str {
2931    let trimmed = entry.trim();
2932    if let Some((quoted, end)) = parse_quoted_string_literal(trimmed, 0) {
2933        let next = skip_ascii_whitespace(trimmed, end);
2934        if trimmed.as_bytes().get(next) == Some(&b':') {
2935            return quoted;
2936        }
2937        return "";
2938    }
2939    trimmed
2940        .find(':')
2941        .map(|separator| &trimmed[..separator])
2942        .unwrap_or(trimmed)
2943        .trim()
2944}
2945
2946fn parse_quoted_string_literal(source: &str, index: usize) -> Option<(&str, usize)> {
2947    let quote = *source.as_bytes().get(index)?;
2948    if quote != b'\'' && quote != b'"' {
2949        return None;
2950    }
2951    let mut cursor = index + 1;
2952    while cursor < source.len() {
2953        let byte = source.as_bytes()[cursor];
2954        if byte == b'\\' {
2955            cursor = cursor.saturating_add(2);
2956            continue;
2957        }
2958        if byte == quote {
2959            let value = &source[index + 1..cursor];
2960            return Some((value, cursor + 1));
2961        }
2962        cursor += 1;
2963    }
2964    None
2965}
2966
2967/// Whether CJS `source` re-exports names through a runtime pattern that static scanning in
2968/// [`extract_cjs_export_names`] cannot resolve, so the named-export set is provably incomplete
2969/// without evaluating the module. Covers tsc/tslib's `__exportStar(require("./sub"), exports)`
2970/// helper (which copies a submodule's enumerable keys onto `exports` at runtime) and
2971/// bulk exports whose final enumerable keys can depend on runtime values.
2972fn source_has_dynamic_cjs_reexports(source: &str) -> bool {
2973    source.contains("__exportStar")
2974        || source.contains("Object.assign(exports")
2975        || source.contains("Object.assign(module.exports")
2976        || source.contains("Object.defineProperties(exports")
2977        || source.contains("Object.defineProperties(module.exports")
2978        || source_has_module_exports_object_spread(source)
2979}
2980
2981fn source_has_module_exports_object_spread(source: &str) -> bool {
2982    let mut cursor = 0usize;
2983    while let Some(start) = find_code_pattern(source, "module.exports", cursor) {
2984        let mut index = skip_ascii_whitespace(source, start + "module.exports".len());
2985        if source.as_bytes().get(index) != Some(&b'=') {
2986            cursor = start + "module.exports".len();
2987            continue;
2988        }
2989        index = skip_ascii_whitespace(source, index + 1);
2990        if source.as_bytes().get(index) != Some(&b'{') {
2991            cursor = index.saturating_add(1);
2992            continue;
2993        }
2994        let end = collect_object_literal_keys(source, index, &mut HashSet::new());
2995        if source[index..end.min(source.len())].contains("...") {
2996            return true;
2997        }
2998        cursor = end;
2999    }
3000    false
3001}
3002
3003fn add_esm_runtime_prelude(source: &str) -> String {
3004    let mut prelude = String::new();
3005
3006    if source.contains("require(")
3007        && !source.contains("createRequire(import.meta.url)")
3008        && !source.contains("createRequire(")
3009        && !source.contains("const require =")
3010        && !source.contains("let require =")
3011        && !source.contains("var require =")
3012        && !source.contains("function require(")
3013    {
3014        prelude
3015            .push_str("const require = globalThis._moduleModule.createRequire(import.meta.url);\n");
3016    }
3017
3018    for (name, triggers) in [
3019        ("fetch", &["fetch("][..]),
3020        ("Headers", &["Headers", "new Headers("][..]),
3021        ("Request", &["Request", "new Request("][..]),
3022        ("Response", &["Response", "new Response("][..]),
3023        ("Blob", &["Blob", "new Blob("][..]),
3024        ("File", &["File", "new File("][..]),
3025        ("FormData", &["FormData", "new FormData("][..]),
3026    ] {
3027        if needs_esm_global_alias(source, name, triggers) {
3028            prelude.push_str(&format!("const {name} = globalThis.{name};\n"));
3029        }
3030    }
3031
3032    if prelude.is_empty() {
3033        source.to_owned()
3034    } else {
3035        format!("{prelude}{source}")
3036    }
3037}
3038
3039fn needs_esm_global_alias(source: &str, name: &str, triggers: &[&str]) -> bool {
3040    if !triggers.iter().any(|trigger| source.contains(trigger)) {
3041        return false;
3042    }
3043
3044    if has_named_import_binding(source, name) {
3045        return false;
3046    }
3047
3048    for pattern in [
3049        format!("const {name}"),
3050        format!("let {name}"),
3051        format!("var {name}"),
3052        format!("function {name}"),
3053        format!("class {name}"),
3054        format!("import {name} from"),
3055        format!("import * as {name}"),
3056    ] {
3057        if source.contains(&pattern) {
3058            return false;
3059        }
3060    }
3061
3062    true
3063}
3064
3065fn has_named_import_binding(source: &str, name: &str) -> bool {
3066    #[derive(Clone, Copy, PartialEq, Eq)]
3067    enum ScanState {
3068        Code,
3069        LineComment,
3070        BlockComment,
3071        SingleQuote,
3072        DoubleQuote,
3073        Template,
3074    }
3075
3076    let bytes = source.as_bytes();
3077    let mut state = ScanState::Code;
3078    let mut index = 0usize;
3079
3080    while index < bytes.len() {
3081        let byte = bytes[index];
3082        let next = bytes.get(index + 1).copied();
3083
3084        match state {
3085            ScanState::Code => {
3086                if byte == b'/' && next == Some(b'/') {
3087                    state = ScanState::LineComment;
3088                    index += 2;
3089                    continue;
3090                }
3091                if byte == b'/' && next == Some(b'*') {
3092                    state = ScanState::BlockComment;
3093                    index += 2;
3094                    continue;
3095                }
3096                if byte == b'\'' {
3097                    state = ScanState::SingleQuote;
3098                    index += 1;
3099                    continue;
3100                }
3101                if byte == b'"' {
3102                    state = ScanState::DoubleQuote;
3103                    index += 1;
3104                    continue;
3105                }
3106                if byte == b'`' {
3107                    state = ScanState::Template;
3108                    index += 1;
3109                    continue;
3110                }
3111                if !is_js_ident_start(byte) {
3112                    index += 1;
3113                    continue;
3114                }
3115
3116                let start = index;
3117                index += 1;
3118                while index < bytes.len() && is_js_ident_continue(bytes[index]) {
3119                    index += 1;
3120                }
3121                if &source[start..index] != "import" {
3122                    continue;
3123                }
3124
3125                let mut cursor = index;
3126                while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
3127                    cursor += 1;
3128                }
3129                if bytes.get(cursor).copied() != Some(b'{') {
3130                    continue;
3131                }
3132                cursor += 1;
3133                let imports_start = cursor;
3134                while cursor < bytes.len() && bytes[cursor] != b'}' {
3135                    cursor += 1;
3136                }
3137                if cursor >= bytes.len() {
3138                    return false;
3139                }
3140                if named_imports_bind_name(&source[imports_start..cursor], name) {
3141                    return true;
3142                }
3143                index = cursor + 1;
3144            }
3145            ScanState::LineComment => {
3146                if byte == b'\n' {
3147                    state = ScanState::Code;
3148                }
3149                index += 1;
3150            }
3151            ScanState::BlockComment => {
3152                if byte == b'*' && next == Some(b'/') {
3153                    state = ScanState::Code;
3154                    index += 2;
3155                } else {
3156                    index += 1;
3157                }
3158            }
3159            ScanState::SingleQuote => {
3160                if byte == b'\\' {
3161                    index += 2;
3162                } else if byte == b'\'' {
3163                    state = ScanState::Code;
3164                    index += 1;
3165                } else {
3166                    index += 1;
3167                }
3168            }
3169            ScanState::DoubleQuote => {
3170                if byte == b'\\' {
3171                    index += 2;
3172                } else if byte == b'"' {
3173                    state = ScanState::Code;
3174                    index += 1;
3175                } else {
3176                    index += 1;
3177                }
3178            }
3179            ScanState::Template => {
3180                if byte == b'\\' {
3181                    index += 2;
3182                } else if byte == b'`' {
3183                    state = ScanState::Code;
3184                    index += 1;
3185                } else {
3186                    index += 1;
3187                }
3188            }
3189        }
3190    }
3191    false
3192}
3193
3194fn named_imports_bind_name(imports: &str, name: &str) -> bool {
3195    imports.split(',').any(|part| {
3196        let local = part
3197            .split_once(" as ")
3198            .map(|(_, alias)| alias)
3199            .unwrap_or(part);
3200        local.trim() == name
3201    })
3202}
3203
3204fn is_valid_js_ident(s: &str) -> bool {
3205    if s.is_empty() {
3206        return false;
3207    }
3208    if is_js_reserved_word(s) {
3209        return false;
3210    }
3211    let mut chars = s.chars();
3212    let first = chars.next().unwrap();
3213    if !first.is_alphabetic() && first != '_' && first != '$' {
3214        return false;
3215    }
3216    chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
3217}
3218
3219fn is_js_reserved_word(s: &str) -> bool {
3220    matches!(
3221        s,
3222        "arguments"
3223            | "as"
3224            | "async"
3225            | "await"
3226            | "break"
3227            | "case"
3228            | "catch"
3229            | "class"
3230            | "const"
3231            | "continue"
3232            | "debugger"
3233            | "default"
3234            | "delete"
3235            | "do"
3236            | "else"
3237            | "enum"
3238            | "eval"
3239            | "export"
3240            | "extends"
3241            | "false"
3242            | "finally"
3243            | "for"
3244            | "from"
3245            | "function"
3246            | "get"
3247            | "if"
3248            | "implements"
3249            | "import"
3250            | "in"
3251            | "instanceof"
3252            | "interface"
3253            | "let"
3254            | "new"
3255            | "null"
3256            | "of"
3257            | "package"
3258            | "private"
3259            | "protected"
3260            | "public"
3261            | "return"
3262            | "set"
3263            | "static"
3264            | "super"
3265            | "switch"
3266            | "target"
3267            | "this"
3268            | "throw"
3269            | "true"
3270            | "try"
3271            | "typeof"
3272            | "var"
3273            | "void"
3274            | "while"
3275            | "with"
3276            | "yield"
3277    )
3278}
3279
3280#[cfg(test)]
3281mod tests {
3282    use super::*;
3283    use crate::bridge;
3284    use crate::host_call::BridgeCallContext;
3285    use crate::isolate;
3286    use std::collections::HashMap;
3287    use std::io::{Cursor, Write};
3288    use std::sync::{Arc, Mutex};
3289
3290    #[test]
3291    fn strip_leading_shebang_matches_node() {
3292        // Shebang stripped (newline preserved so line numbers hold).
3293        assert_eq!(
3294            strip_leading_shebang("#!/usr/bin/env node\nexport const x = 1;\n"),
3295            "\nexport const x = 1;\n"
3296        );
3297        // No shebang -> untouched.
3298        assert_eq!(
3299            strip_leading_shebang("export const x = 1;\n"),
3300            "export const x = 1;\n"
3301        );
3302        // `#` not at byte 0 -> untouched (only a leading shebang is special).
3303        assert_eq!(strip_leading_shebang("  #!nope\n"), "  #!nope\n");
3304        // Whole file is just a shebang.
3305        assert_eq!(strip_leading_shebang("#!/usr/bin/env node"), "");
3306    }
3307
3308    /// Shared writer that captures output for test inspection
3309    struct SharedWriter(Arc<Mutex<Vec<u8>>>);
3310
3311    impl Write for SharedWriter {
3312        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3313            self.0.lock().unwrap().write(buf)
3314        }
3315        fn flush(&mut self) -> std::io::Result<()> {
3316            self.0.lock().unwrap().flush()
3317        }
3318    }
3319
3320    #[test]
3321    fn esm_global_alias_detection_handles_multiline_named_imports() {
3322        let source = r#"
3323import {
3324  Blob,
3325  File,
3326  FormData
3327} from "fetch-blob/from.js";
3328
3329export { File };
3330"#;
3331
3332        assert!(!needs_esm_global_alias(source, "File", &["File"]));
3333    }
3334
3335    #[test]
3336    fn esm_global_alias_detection_handles_named_import_aliases() {
3337        let source = r#"
3338import {
3339  File as RuntimeFile
3340} from "fetch-blob/from.js";
3341
3342export const file = RuntimeFile;
3343"#;
3344
3345        assert!(!needs_esm_global_alias(
3346            source,
3347            "RuntimeFile",
3348            &["RuntimeFile"]
3349        ));
3350    }
3351
3352    #[test]
3353    fn esm_global_alias_detection_ignores_commented_named_imports() {
3354        let source = r#"
3355// import { File } from "fetch-blob/from.js";
3356/*
3357import {
3358  Blob,
3359  File
3360} from "fetch-blob/from.js";
3361*/
3362export function makeFile() {
3363  return new File([], "empty.txt");
3364}
3365"#;
3366
3367        assert!(needs_esm_global_alias(source, "File", &["new File("]));
3368    }
3369
3370    #[test]
3371    fn esm_global_alias_detection_ignores_string_named_imports() {
3372        let source = r#"
3373const example = "import { File } from 'fetch-blob/from.js'";
3374const singleQuoteExample = 'import { File } from "fetch-blob/from.js"';
3375const template = `import {
3376  File
3377} from "fetch-blob/from.js"`;
3378
3379export const file = new File([], "empty.txt");
3380"#;
3381
3382        assert!(needs_esm_global_alias(source, "File", &["new File("]));
3383    }
3384
3385    /// Helper: serialize a V8 string value for test BridgeResponse payloads
3386    fn v8_serialize_str(
3387        iso: &mut v8::OwnedIsolate,
3388        ctx: &v8::Global<v8::Context>,
3389        s: &str,
3390    ) -> Vec<u8> {
3391        let scope = &mut v8::HandleScope::new(iso);
3392        let local = v8::Local::new(scope, ctx);
3393        let scope = &mut v8::ContextScope::new(scope, local);
3394        let val = v8::String::new(scope, s).unwrap();
3395        crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3396    }
3397
3398    /// Helper: serialize a V8 integer value for test BridgeResponse payloads
3399    fn v8_serialize_int(
3400        iso: &mut v8::OwnedIsolate,
3401        ctx: &v8::Global<v8::Context>,
3402        n: i64,
3403    ) -> Vec<u8> {
3404        let scope = &mut v8::HandleScope::new(iso);
3405        let local = v8::Local::new(scope, ctx);
3406        let scope = &mut v8::ContextScope::new(scope, local);
3407        let val = v8::Number::new(scope, n as f64);
3408        crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3409    }
3410
3411    /// Helper: serialize a V8 null value for test BridgeResponse payloads
3412    fn v8_serialize_null(iso: &mut v8::OwnedIsolate, ctx: &v8::Global<v8::Context>) -> Vec<u8> {
3413        let scope = &mut v8::HandleScope::new(iso);
3414        let local = v8::Local::new(scope, ctx);
3415        let scope = &mut v8::ContextScope::new(scope, local);
3416        let val = v8::null(scope);
3417        crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3418    }
3419
3420    /// Helper: serialize a V8 object (from JS expression) for test BridgeResponse payloads
3421    fn v8_serialize_eval(
3422        iso: &mut v8::OwnedIsolate,
3423        ctx: &v8::Global<v8::Context>,
3424        expr: &str,
3425    ) -> Vec<u8> {
3426        let scope = &mut v8::HandleScope::new(iso);
3427        let local = v8::Local::new(scope, ctx);
3428        let scope = &mut v8::ContextScope::new(scope, local);
3429        let source = v8::String::new(scope, expr).unwrap();
3430        let script = v8::Script::compile(scope, source, None).unwrap();
3431        let val = script.run(scope).unwrap();
3432        crate::bridge::serialize_v8_value(scope, val).unwrap()
3433    }
3434
3435    /// Enter a context, run JS, return the string result.
3436    fn eval(
3437        isolate: &mut v8::OwnedIsolate,
3438        context: &v8::Global<v8::Context>,
3439        code: &str,
3440    ) -> String {
3441        let scope = &mut v8::HandleScope::new(isolate);
3442        let local = v8::Local::new(scope, context);
3443        let scope = &mut v8::ContextScope::new(scope, local);
3444        let source = v8::String::new(scope, code).unwrap();
3445        let script = v8::Script::compile(scope, source, None).unwrap();
3446        let result = script.run(scope).unwrap();
3447        result.to_rust_string_lossy(scope)
3448    }
3449
3450    /// Enter a context, run JS, return true if the result is truthy.
3451    fn eval_bool(
3452        isolate: &mut v8::OwnedIsolate,
3453        context: &v8::Global<v8::Context>,
3454        code: &str,
3455    ) -> bool {
3456        let scope = &mut v8::HandleScope::new(isolate);
3457        let local = v8::Local::new(scope, context);
3458        let scope = &mut v8::ContextScope::new(scope, local);
3459        let source = v8::String::new(scope, code).unwrap();
3460        let script = v8::Script::compile(scope, source, None).unwrap();
3461        let result = script.run(scope).unwrap();
3462        result.boolean_value(scope)
3463    }
3464
3465    /// Enter a context, run JS, return true if an exception was thrown.
3466    fn eval_throws(
3467        isolate: &mut v8::OwnedIsolate,
3468        context: &v8::Global<v8::Context>,
3469        code: &str,
3470    ) -> bool {
3471        let scope = &mut v8::HandleScope::new(isolate);
3472        let local = v8::Local::new(scope, context);
3473        let scope = &mut v8::ContextScope::new(scope, local);
3474        let tc = &mut v8::TryCatch::new(scope);
3475        let source = v8::String::new(tc, code).unwrap();
3476        if let Some(script) = v8::Script::compile(tc, source, None) {
3477            script.run(tc);
3478        }
3479        tc.has_caught()
3480    }
3481
3482    #[test]
3483    fn v8_consolidated_tests() {
3484        isolate::init_v8_platform();
3485
3486        // --- Isolate lifecycle (moved from isolate::tests to consolidate V8 tests) ---
3487        // Create and destroy 3 isolates sequentially without crash
3488        for i in 0..3 {
3489            let mut isolate = isolate::create_isolate(None);
3490            let context = isolate::create_context(&mut isolate);
3491            let result = eval(&mut isolate, &context, &format!("{} + 1", i));
3492            assert_eq!(result, format!("{}", i + 1));
3493        }
3494        // Isolate with heap limit
3495        {
3496            let mut isolate = isolate::create_isolate(Some(16));
3497            let context = isolate::create_context(&mut isolate);
3498            assert_eq!(eval(&mut isolate, &context, "1 + 2"), "3");
3499        }
3500        // Isolate without heap limit
3501        {
3502            let mut isolate = isolate::create_isolate(None);
3503            let context = isolate::create_context(&mut isolate);
3504            assert_eq!(
3505                eval(&mut isolate, &context, "'hello' + ' world'"),
3506                "hello world"
3507            );
3508        }
3509        // Global context handle persists state
3510        {
3511            let mut isolate = isolate::create_isolate(None);
3512            let context = isolate::create_context(&mut isolate);
3513            eval(&mut isolate, &context, "var x = 42;");
3514            assert_eq!(eval(&mut isolate, &context, "x"), "42");
3515        }
3516        // Unhandled rejection tracking is bounded within a microtask checkpoint.
3517        {
3518            let mut isolate = isolate::create_isolate(None);
3519            let context = isolate::create_context(&mut isolate);
3520            let (code, error) = {
3521                let scope = &mut v8::HandleScope::new(&mut isolate);
3522                let ctx = v8::Local::new(scope, &context);
3523                let scope = &mut v8::ContextScope::new(scope, ctx);
3524                execute_script(
3525                    scope,
3526                    "",
3527                    "for (let i = 0; i < 1100; i++) Promise.reject(new Error('boom ' + i));",
3528                    &mut None,
3529                )
3530            };
3531            assert_eq!(code, 1);
3532            let error = error.expect("unhandled rejection limit error");
3533            assert_eq!(
3534                error.code.as_deref(),
3535                Some("ERR_AGENTOS_UNHANDLED_REJECTION_LIMIT")
3536            );
3537            assert!(error
3538                .message
3539                .contains("unhandled promise rejection registry exceeded limit"));
3540        }
3541        // Over-cap rejections that are handled before the drain should not fail.
3542        {
3543            let mut isolate = isolate::create_isolate(None);
3544            let context = isolate::create_context(&mut isolate);
3545            let (code, error) = {
3546                let scope = &mut v8::HandleScope::new(&mut isolate);
3547                let ctx = v8::Local::new(scope, &context);
3548                let scope = &mut v8::ContextScope::new(scope, ctx);
3549                execute_script(
3550                    scope,
3551                    "",
3552                    r#"
3553                    const promises = [];
3554                    for (let i = 0; i < 1100; i++) promises.push(Promise.reject(new Error('boom ' + i)));
3555                    for (const promise of promises) promise.catch(() => {});
3556                    "#,
3557                    &mut None,
3558                )
3559            };
3560            assert_eq!(code, 0);
3561            assert!(
3562                error.is_none(),
3563                "handled over-cap rejections should not surface a limit error"
3564            );
3565        }
3566
3567        // --- Part 1: InjectGlobals sets _processConfig and _osConfig ---
3568        {
3569            let mut isolate = isolate::create_isolate(None);
3570            let context = isolate::create_context(&mut isolate);
3571
3572            let mut env = HashMap::new();
3573            env.insert("HOME".into(), "/home/agentos".into());
3574            env.insert("PATH".into(), "/usr/bin".into());
3575
3576            let process_config = ProcessConfig {
3577                cwd: "/app".into(),
3578                env,
3579                timing_mitigation: "none".into(),
3580                frozen_time_ms: Some(1700000000000.0),
3581                high_resolution_time: true,
3582            };
3583            let os_config = OsConfig {
3584                homedir: "/home/agentos".into(),
3585                tmpdir: "/tmp".into(),
3586                platform: "linux".into(),
3587                arch: "x64".into(),
3588            };
3589
3590            // Inject globals
3591            {
3592                let scope = &mut v8::HandleScope::new(&mut isolate);
3593                let ctx = v8::Local::new(scope, &context);
3594                let scope = &mut v8::ContextScope::new(scope, ctx);
3595                inject_globals(scope, &process_config, &os_config);
3596            }
3597
3598            // Verify _processConfig values
3599            assert_eq!(eval(&mut isolate, &context, "_processConfig.cwd"), "/app");
3600            assert_eq!(
3601                eval(&mut isolate, &context, "_processConfig.timing_mitigation"),
3602                "none"
3603            );
3604            assert_eq!(
3605                eval(&mut isolate, &context, "_processConfig.frozen_time_ms"),
3606                "1700000000000"
3607            );
3608            assert_eq!(
3609                eval(
3610                    &mut isolate,
3611                    &context,
3612                    "_processConfig.high_resolution_time"
3613                ),
3614                "true"
3615            );
3616            assert_eq!(
3617                eval(&mut isolate, &context, "_processConfig.env.HOME"),
3618                "/home/agentos"
3619            );
3620            assert_eq!(
3621                eval(&mut isolate, &context, "_processConfig.env.PATH"),
3622                "/usr/bin"
3623            );
3624
3625            // Verify _osConfig values
3626            assert_eq!(
3627                eval(&mut isolate, &context, "_osConfig.homedir"),
3628                "/home/agentos"
3629            );
3630            assert_eq!(eval(&mut isolate, &context, "_osConfig.tmpdir"), "/tmp");
3631            assert_eq!(eval(&mut isolate, &context, "_osConfig.platform"), "linux");
3632            assert_eq!(eval(&mut isolate, &context, "_osConfig.arch"), "x64");
3633        }
3634
3635        // --- Part 1a: InjectGlobals payload injection fails closed on invalid payload ---
3636        {
3637            let mut isolate = isolate::create_isolate(None);
3638            let context = isolate::create_context(&mut isolate);
3639            let payload = v8_serialize_eval(
3640                &mut isolate,
3641                &context,
3642                r#"({
3643                    processConfig: {
3644                        cwd: "/app",
3645                        env: { HOME: "/home/agentos" },
3646                        timing_mitigation: "none",
3647                        frozen_time_ms: null
3648                    }
3649                })"#,
3650            );
3651
3652            let err = {
3653                let scope = &mut v8::HandleScope::new(&mut isolate);
3654                let ctx = v8::Local::new(scope, &context);
3655                let scope = &mut v8::ContextScope::new(scope, ctx);
3656                inject_globals_from_payload(scope, &payload).expect_err("missing osConfig")
3657            };
3658
3659            assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3660            assert!(
3661                err.message.contains("missing osConfig"),
3662                "unexpected error message: {}",
3663                err.message
3664            );
3665            assert_eq!(
3666                eval(&mut isolate, &context, "typeof _processConfig"),
3667                "undefined",
3668                "invalid payload must not partially inject process config"
3669            );
3670            assert_eq!(
3671                eval(&mut isolate, &context, "typeof _osConfig"),
3672                "undefined",
3673                "invalid payload must not inject os config"
3674            );
3675        }
3676
3677        // --- Part 1b: InjectGlobals payload injection rejects primitive configs ---
3678        {
3679            let mut isolate = isolate::create_isolate(None);
3680            let context = isolate::create_context(&mut isolate);
3681            let payload = v8_serialize_eval(
3682                &mut isolate,
3683                &context,
3684                r#"({
3685                    processConfig: "not-an-object",
3686                    osConfig: {
3687                        homedir: "/home/agentos",
3688                        tmpdir: "/tmp",
3689                        platform: "linux",
3690                        arch: "x64"
3691                    }
3692                })"#,
3693            );
3694
3695            let err = {
3696                let scope = &mut v8::HandleScope::new(&mut isolate);
3697                let ctx = v8::Local::new(scope, &context);
3698                let scope = &mut v8::ContextScope::new(scope, ctx);
3699                inject_globals_from_payload(scope, &payload).expect_err("primitive processConfig")
3700            };
3701
3702            assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3703            assert!(
3704                err.message.contains("processConfig is not an object"),
3705                "unexpected error message: {}",
3706                err.message
3707            );
3708            assert_eq!(
3709                eval(&mut isolate, &context, "typeof _processConfig"),
3710                "undefined",
3711                "wrong-type payload must not inject primitive process config"
3712            );
3713        }
3714
3715        // --- Part 1c: InjectGlobals payload injection freezes configs and env ---
3716        {
3717            let mut isolate = isolate::create_isolate(None);
3718            let context = isolate::create_context(&mut isolate);
3719            let payload = v8_serialize_eval(
3720                &mut isolate,
3721                &context,
3722                r#"({
3723                    processConfig: {
3724                        cwd: "/app",
3725                        env: "not-an-object",
3726                        timing_mitigation: "none",
3727                        frozen_time_ms: null
3728                    },
3729                    osConfig: {
3730                        homedir: "/home/agentos",
3731                        tmpdir: "/tmp",
3732                        platform: "linux",
3733                        arch: "x64"
3734                    }
3735                })"#,
3736            );
3737
3738            let err = {
3739                let scope = &mut v8::HandleScope::new(&mut isolate);
3740                let ctx = v8::Local::new(scope, &context);
3741                let scope = &mut v8::ContextScope::new(scope, ctx);
3742                inject_globals_from_payload(scope, &payload).expect_err("primitive env")
3743            };
3744
3745            assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3746            assert!(
3747                err.message.contains("processConfig.env is not an object"),
3748                "unexpected error message: {}",
3749                err.message
3750            );
3751            assert_eq!(
3752                eval(&mut isolate, &context, "typeof _processConfig"),
3753                "undefined",
3754                "wrong-type env payload must not partially inject process config"
3755            );
3756        }
3757
3758        // --- Part 1d: InjectGlobals payload injection rejects missing env ---
3759        {
3760            let mut isolate = isolate::create_isolate(None);
3761            let context = isolate::create_context(&mut isolate);
3762            let payload = v8_serialize_eval(
3763                &mut isolate,
3764                &context,
3765                r#"({
3766                    processConfig: {
3767                        cwd: "/app",
3768                        timing_mitigation: "none",
3769                        frozen_time_ms: null
3770                    },
3771                    osConfig: {
3772                        homedir: "/home/agentos",
3773                        tmpdir: "/tmp",
3774                        platform: "linux",
3775                        arch: "x64"
3776                    }
3777                })"#,
3778            );
3779
3780            let err = {
3781                let scope = &mut v8::HandleScope::new(&mut isolate);
3782                let ctx = v8::Local::new(scope, &context);
3783                let scope = &mut v8::ContextScope::new(scope, ctx);
3784                inject_globals_from_payload(scope, &payload).expect_err("missing env")
3785            };
3786
3787            assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3788            assert!(
3789                err.message.contains("missing processConfig.env"),
3790                "unexpected error message: {}",
3791                err.message
3792            );
3793            assert_eq!(
3794                eval(&mut isolate, &context, "typeof _processConfig"),
3795                "undefined",
3796                "missing env payload must not partially inject process config"
3797            );
3798        }
3799
3800        // --- Part 1e: InjectGlobals payload injection rejects non-plain object env ---
3801        {
3802            let mut isolate = isolate::create_isolate(None);
3803            let context = isolate::create_context(&mut isolate);
3804            let payload = v8_serialize_eval(
3805                &mut isolate,
3806                &context,
3807                r#"({
3808                    processConfig: {
3809                        cwd: "/app",
3810                        env: new Uint8Array([1]),
3811                        timing_mitigation: "none",
3812                        frozen_time_ms: null
3813                    },
3814                    osConfig: {
3815                        homedir: "/home/agentos",
3816                        tmpdir: "/tmp",
3817                        platform: "linux",
3818                        arch: "x64"
3819                    }
3820                })"#,
3821            );
3822
3823            let err = {
3824                let scope = &mut v8::HandleScope::new(&mut isolate);
3825                let ctx = v8::Local::new(scope, &context);
3826                let scope = &mut v8::ContextScope::new(scope, ctx);
3827                inject_globals_from_payload(scope, &payload).expect_err("typed array env")
3828            };
3829
3830            assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3831            assert!(
3832                err.message
3833                    .contains("processConfig.env is not a plain object"),
3834                "unexpected error message: {}",
3835                err.message
3836            );
3837            assert_eq!(
3838                eval(&mut isolate, &context, "typeof _processConfig"),
3839                "undefined",
3840                "typed-array env payload must not partially inject process config"
3841            );
3842        }
3843
3844        // --- Part 1f: InjectGlobals payload injection freezes configs and env ---
3845        {
3846            let mut isolate = isolate::create_isolate(None);
3847            let context = isolate::create_context(&mut isolate);
3848            let payload = v8_serialize_eval(
3849                &mut isolate,
3850                &context,
3851                r#"({
3852                    processConfig: {
3853                        cwd: "/app",
3854                        env: { HOME: "/home/agentos" },
3855                        timing_mitigation: "none",
3856                        frozen_time_ms: null
3857                    },
3858                    osConfig: {
3859                        homedir: "/home/agentos",
3860                        tmpdir: "/tmp",
3861                        platform: "linux",
3862                        arch: "x64"
3863                    }
3864                })"#,
3865            );
3866
3867            {
3868                let scope = &mut v8::HandleScope::new(&mut isolate);
3869                let ctx = v8::Local::new(scope, &context);
3870                let scope = &mut v8::ContextScope::new(scope, ctx);
3871                inject_globals_from_payload(scope, &payload).expect("valid globals payload");
3872            }
3873
3874            assert_eq!(eval(&mut isolate, &context, "_processConfig.cwd"), "/app");
3875            assert_eq!(
3876                eval(&mut isolate, &context, "_processConfig.env.HOME"),
3877                "/home/agentos"
3878            );
3879            assert!(eval_bool(
3880                &mut isolate,
3881                &context,
3882                "Object.isFrozen(_processConfig) && Object.isFrozen(_processConfig.env) && Object.isFrozen(_osConfig)"
3883            ));
3884        }
3885
3886        // --- Part 2: frozen_time_ms null when None ---
3887        {
3888            let mut isolate = isolate::create_isolate(None);
3889            let context = isolate::create_context(&mut isolate);
3890
3891            let process_config = ProcessConfig {
3892                cwd: "/".into(),
3893                env: HashMap::new(),
3894                timing_mitigation: "none".into(),
3895                frozen_time_ms: None,
3896                high_resolution_time: false,
3897            };
3898            let os_config = OsConfig {
3899                homedir: "/root".into(),
3900                tmpdir: "/tmp".into(),
3901                platform: "linux".into(),
3902                arch: "x64".into(),
3903            };
3904
3905            {
3906                let scope = &mut v8::HandleScope::new(&mut isolate);
3907                let ctx = v8::Local::new(scope, &context);
3908                let scope = &mut v8::ContextScope::new(scope, ctx);
3909                inject_globals(scope, &process_config, &os_config);
3910            }
3911
3912            assert_eq!(
3913                eval(
3914                    &mut isolate,
3915                    &context,
3916                    "_processConfig.frozen_time_ms === null"
3917                ),
3918                "true"
3919            );
3920        }
3921
3922        // --- Part 3: Objects are frozen (immutable) ---
3923        {
3924            let mut isolate = isolate::create_isolate(None);
3925            let context = isolate::create_context(&mut isolate);
3926
3927            let process_config = ProcessConfig {
3928                cwd: "/app".into(),
3929                env: HashMap::new(),
3930                timing_mitigation: "none".into(),
3931                frozen_time_ms: None,
3932                high_resolution_time: false,
3933            };
3934            let os_config = OsConfig {
3935                homedir: "/home".into(),
3936                tmpdir: "/tmp".into(),
3937                platform: "linux".into(),
3938                arch: "x64".into(),
3939            };
3940
3941            {
3942                let scope = &mut v8::HandleScope::new(&mut isolate);
3943                let ctx = v8::Local::new(scope, &context);
3944                let scope = &mut v8::ContextScope::new(scope, ctx);
3945                inject_globals(scope, &process_config, &os_config);
3946            }
3947
3948            // Verify Object.isFrozen
3949            assert!(eval_bool(
3950                &mut isolate,
3951                &context,
3952                "Object.isFrozen(_processConfig)"
3953            ));
3954            assert!(eval_bool(
3955                &mut isolate,
3956                &context,
3957                "Object.isFrozen(_osConfig)"
3958            ));
3959            assert!(eval_bool(
3960                &mut isolate,
3961                &context,
3962                "Object.isFrozen(_processConfig.env)"
3963            ));
3964
3965            // Verify non-writable: assignment in strict mode throws
3966            assert!(eval_throws(
3967                &mut isolate,
3968                &context,
3969                "'use strict'; _processConfig.cwd = '/hacked'"
3970            ));
3971            assert!(eval_throws(
3972                &mut isolate,
3973                &context,
3974                "'use strict'; _osConfig.platform = 'hacked'"
3975            ));
3976
3977            // Verify non-configurable: cannot delete or redefine
3978            assert!(eval_throws(
3979                &mut isolate,
3980                &context,
3981                "'use strict'; delete _processConfig"
3982            ));
3983            assert!(eval_throws(
3984                &mut isolate,
3985                &context,
3986                "Object.defineProperty(globalThis, '_processConfig', { value: {} })"
3987            ));
3988            assert!(eval_throws(
3989                &mut isolate,
3990                &context,
3991                "Object.defineProperty(globalThis, '_osConfig', { value: {} })"
3992            ));
3993        }
3994
3995        // --- Part 4: SharedArrayBuffer NOT removed by inject_globals ---
3996        // SharedArrayBuffer removal is handled by JS bridge code (applyTimingMitigationFreeze),
3997        // not by inject_globals. The bridge bundle depends on SharedArrayBuffer being available
3998        // during initialization. inject_globals stores timing_mitigation in _processConfig
3999        // for the bridge to read.
4000        {
4001            let mut isolate = isolate::create_isolate(None);
4002            let context = isolate::create_context(&mut isolate);
4003
4004            let process_config = ProcessConfig {
4005                cwd: "/".into(),
4006                env: HashMap::new(),
4007                timing_mitigation: "freeze".into(),
4008                frozen_time_ms: None,
4009                high_resolution_time: false,
4010            };
4011            let os_config = OsConfig {
4012                homedir: "/root".into(),
4013                tmpdir: "/tmp".into(),
4014                platform: "linux".into(),
4015                arch: "x64".into(),
4016            };
4017
4018            {
4019                let scope = &mut v8::HandleScope::new(&mut isolate);
4020                let ctx = v8::Local::new(scope, &context);
4021                let scope = &mut v8::ContextScope::new(scope, ctx);
4022                inject_globals(scope, &process_config, &os_config);
4023            }
4024
4025            // SharedArrayBuffer should still exist — removal is done by JS bridge
4026            assert!(eval_bool(
4027                &mut isolate,
4028                &context,
4029                "typeof SharedArrayBuffer !== 'undefined'"
4030            ));
4031            // timing_mitigation is stored for the bridge to act on
4032            assert_eq!(
4033                eval(&mut isolate, &context, "_processConfig.timing_mitigation"),
4034                "freeze"
4035            );
4036        }
4037
4038        // --- Part 5: SharedArrayBuffer preserved when timing_mitigation is 'none' ---
4039        {
4040            let mut isolate = isolate::create_isolate(None);
4041            let context = isolate::create_context(&mut isolate);
4042
4043            let process_config = ProcessConfig {
4044                cwd: "/".into(),
4045                env: HashMap::new(),
4046                timing_mitigation: "none".into(),
4047                frozen_time_ms: None,
4048                high_resolution_time: false,
4049            };
4050            let os_config = OsConfig {
4051                homedir: "/root".into(),
4052                tmpdir: "/tmp".into(),
4053                platform: "linux".into(),
4054                arch: "x64".into(),
4055            };
4056
4057            {
4058                let scope = &mut v8::HandleScope::new(&mut isolate);
4059                let ctx = v8::Local::new(scope, &context);
4060                let scope = &mut v8::ContextScope::new(scope, ctx);
4061                inject_globals(scope, &process_config, &os_config);
4062            }
4063
4064            // SharedArrayBuffer should still exist
4065            assert!(eval_bool(
4066                &mut isolate,
4067                &context,
4068                "typeof SharedArrayBuffer !== 'undefined'"
4069            ));
4070        }
4071
4072        // --- Part 6: Guest WebAssembly compilation stays enabled by default ---
4073        {
4074            let mut isolate = isolate::create_isolate(None);
4075            let context = isolate::create_context(&mut isolate);
4076
4077            assert!(!eval_throws(
4078                &mut isolate,
4079                &context,
4080                "new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0]))"
4081            ));
4082        }
4083
4084        // --- Part 7: Guest WebAssembly modules can instantiate and execute ---
4085        {
4086            let mut isolate = isolate::create_isolate(None);
4087            let context = isolate::create_context(&mut isolate);
4088
4089            let result = eval(
4090                &mut isolate,
4091                &context,
4092                r#"
4093                (function() {
4094                    var bytes = new Uint8Array([
4095                        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
4096                        0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
4097                        0x03, 0x02, 0x01, 0x00,
4098                        0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
4099                        0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
4100                    ]);
4101                    var module = new WebAssembly.Module(bytes);
4102                    var instance = new WebAssembly.Instance(module, {});
4103                    return String(instance.exports.add(19, 23));
4104                })()
4105                "#,
4106            );
4107            assert_eq!(result, "42");
4108        }
4109
4110        // --- Part 8: V8 still enforces its own WebAssembly memory limits ---
4111        {
4112            let mut isolate = isolate::create_isolate(None);
4113            let context = isolate::create_context(&mut isolate);
4114
4115            let limit_report = eval(
4116                &mut isolate,
4117                &context,
4118                r#"
4119                (function() {
4120                    function capture(fn) {
4121                        try {
4122                            fn();
4123                            return "ALLOWED";
4124                        } catch (error) {
4125                            return error.name + ":" + error.message;
4126                        }
4127                    }
4128
4129                    var moduleLimit = capture(function() {
4130                        var bytes = new Uint8Array([
4131                            0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
4132                            0x05, 0x06, 0x01, 0x01, 0x01, 0x81, 0x80, 0x04,
4133                        ]);
4134                        new WebAssembly.Module(bytes);
4135                    });
4136                    var memoryLimit = capture(function() {
4137                        new WebAssembly.Memory({ initial: 1, maximum: 65537 });
4138                    });
4139                    return JSON.stringify({ moduleLimit: moduleLimit, memoryLimit: memoryLimit });
4140                })()
4141                "#,
4142            );
4143
4144            assert!(
4145                limit_report.contains(r#""moduleLimit":"CompileError:"#),
4146                "unexpected module limit report: {limit_report}"
4147            );
4148            assert!(
4149                limit_report.contains(r#""memoryLimit":"RangeError:"#),
4150                "unexpected memory limit report: {limit_report}"
4151            );
4152            assert!(
4153                limit_report.contains("65536"),
4154                "unexpected limit report: {limit_report}"
4155            );
4156        }
4157
4158        // --- Part 8: Sync bridge call returns value ---
4159        {
4160            let mut iso = isolate::create_isolate(None);
4161            let ctx = isolate::create_context(&mut iso);
4162
4163            // Prepare BridgeResponse: call_id=1, result="hello world"
4164            let result_v8 = v8_serialize_str(&mut iso, &ctx, "hello world");
4165
4166            let mut response_buf = Vec::new();
4167            crate::ipc_binary::write_frame(
4168                &mut response_buf,
4169                &crate::ipc_binary::BinaryFrame::BridgeResponse {
4170                    session_id: String::new(),
4171                    call_id: 1,
4172                    status: 0,
4173                    payload: result_v8,
4174                },
4175            )
4176            .unwrap();
4177
4178            let bridge_ctx = BridgeCallContext::new(
4179                Box::new(Vec::new()),
4180                Box::new(Cursor::new(response_buf)),
4181                "test-session".into(),
4182            );
4183
4184            let _fn_store;
4185            {
4186                let scope = &mut v8::HandleScope::new(&mut iso);
4187                let local = v8::Local::new(scope, &ctx);
4188                let scope = &mut v8::ContextScope::new(scope, local);
4189                _fn_store = bridge::register_sync_bridge_fns(
4190                    scope,
4191                    &bridge_ctx as *const BridgeCallContext,
4192                    &["_testBridge"],
4193                );
4194            }
4195
4196            assert_eq!(eval(&mut iso, &ctx, "_testBridge('arg1')"), "hello world");
4197        }
4198
4199        // --- Part 9: Bridge call error throws V8 exception ---
4200        {
4201            let mut iso = isolate::create_isolate(None);
4202            let ctx = isolate::create_context(&mut iso);
4203
4204            let mut response_buf = Vec::new();
4205            crate::ipc_binary::write_frame(
4206                &mut response_buf,
4207                &crate::ipc_binary::BinaryFrame::BridgeResponse {
4208                    session_id: String::new(),
4209                    call_id: 1,
4210                    status: 1,
4211                    payload: "ENOENT: file not found".as_bytes().to_vec(),
4212                },
4213            )
4214            .unwrap();
4215
4216            let bridge_ctx = BridgeCallContext::new(
4217                Box::new(Vec::new()),
4218                Box::new(Cursor::new(response_buf)),
4219                "test-session".into(),
4220            );
4221
4222            let _fn_store;
4223            {
4224                let scope = &mut v8::HandleScope::new(&mut iso);
4225                let local = v8::Local::new(scope, &ctx);
4226                let scope = &mut v8::ContextScope::new(scope, local);
4227                _fn_store = bridge::register_sync_bridge_fns(
4228                    scope,
4229                    &bridge_ctx as *const BridgeCallContext,
4230                    &["_testBridge"],
4231                );
4232            }
4233
4234            assert!(eval_throws(&mut iso, &ctx, "_testBridge('arg')"));
4235        }
4236
4237        // --- Part 10: Multiple bridge functions with argument passing ---
4238        {
4239            let mut iso = isolate::create_isolate(None);
4240            let ctx = isolate::create_context(&mut iso);
4241
4242            // Prepare two BridgeResponses (call_id=1 for _fn1, call_id=2 for _fn2)
4243            let r1_bytes = v8_serialize_str(&mut iso, &ctx, "result-one");
4244            let r2_bytes = v8_serialize_int(&mut iso, &ctx, 42);
4245
4246            let mut response_buf = Vec::new();
4247            crate::ipc_binary::write_frame(
4248                &mut response_buf,
4249                &crate::ipc_binary::BinaryFrame::BridgeResponse {
4250                    session_id: String::new(),
4251                    call_id: 1,
4252                    status: 0,
4253                    payload: r1_bytes,
4254                },
4255            )
4256            .unwrap();
4257            crate::ipc_binary::write_frame(
4258                &mut response_buf,
4259                &crate::ipc_binary::BinaryFrame::BridgeResponse {
4260                    session_id: String::new(),
4261                    call_id: 2,
4262                    status: 0,
4263                    payload: r2_bytes,
4264                },
4265            )
4266            .unwrap();
4267
4268            let bridge_ctx = BridgeCallContext::new(
4269                Box::new(Vec::new()),
4270                Box::new(Cursor::new(response_buf)),
4271                "test-session".into(),
4272            );
4273
4274            let _fn_store;
4275            {
4276                let scope = &mut v8::HandleScope::new(&mut iso);
4277                let local = v8::Local::new(scope, &ctx);
4278                let scope = &mut v8::ContextScope::new(scope, local);
4279                _fn_store = bridge::register_sync_bridge_fns(
4280                    scope,
4281                    &bridge_ctx as *const BridgeCallContext,
4282                    &["_fn1", "_fn2"],
4283                );
4284            }
4285
4286            assert_eq!(eval(&mut iso, &ctx, "_fn1('x')"), "result-one");
4287            assert_eq!(eval(&mut iso, &ctx, "_fn2(1, 2, 3)"), "42");
4288        }
4289
4290        // --- Part 11: Bridge call with null result returns undefined ---
4291        {
4292            let mut iso = isolate::create_isolate(None);
4293            let ctx = isolate::create_context(&mut iso);
4294
4295            let mut response_buf = Vec::new();
4296            crate::ipc_binary::write_frame(
4297                &mut response_buf,
4298                &crate::ipc_binary::BinaryFrame::BridgeResponse {
4299                    session_id: String::new(),
4300                    call_id: 1,
4301                    status: 0,
4302                    payload: vec![],
4303                },
4304            )
4305            .unwrap();
4306
4307            let bridge_ctx = BridgeCallContext::new(
4308                Box::new(Vec::new()),
4309                Box::new(Cursor::new(response_buf)),
4310                "test-session".into(),
4311            );
4312
4313            let _fn_store;
4314            {
4315                let scope = &mut v8::HandleScope::new(&mut iso);
4316                let local = v8::Local::new(scope, &ctx);
4317                let scope = &mut v8::ContextScope::new(scope, local);
4318                _fn_store = bridge::register_sync_bridge_fns(
4319                    scope,
4320                    &bridge_ctx as *const BridgeCallContext,
4321                    &["_testBridge"],
4322                );
4323            }
4324
4325            assert!(eval_bool(&mut iso, &ctx, "_testBridge() === undefined"));
4326        }
4327
4328        // --- Part 12: Async bridge call returns pending promise, resolved successfully ---
4329        {
4330            let mut iso = isolate::create_isolate(None);
4331            let ctx = isolate::create_context(&mut iso);
4332
4333            let writer_buf = Arc::new(Mutex::new(Vec::new()));
4334            let bridge_ctx = BridgeCallContext::new(
4335                Box::new(SharedWriter(Arc::clone(&writer_buf))),
4336                Box::new(Cursor::new(Vec::new())),
4337                "test-session".into(),
4338            );
4339            let pending = bridge::PendingPromises::new();
4340
4341            let _fn_store;
4342            {
4343                let scope = &mut v8::HandleScope::new(&mut iso);
4344                let local = v8::Local::new(scope, &ctx);
4345                let scope = &mut v8::ContextScope::new(scope, local);
4346                _fn_store = bridge::register_async_bridge_fns(
4347                    scope,
4348                    &bridge_ctx as *const BridgeCallContext,
4349                    &pending as *const bridge::PendingPromises,
4350                    &["_asyncFn"],
4351                );
4352            }
4353
4354            // Call the async function
4355            eval(&mut iso, &ctx, "var _promise = _asyncFn('arg1')");
4356
4357            // Verify a BridgeCall was sent
4358            {
4359                let written = writer_buf.lock().unwrap();
4360                let call = crate::ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
4361                match call {
4362                    crate::ipc_binary::BinaryFrame::BridgeCall {
4363                        call_id, method, ..
4364                    } => {
4365                        assert_eq!(call_id, 1);
4366                        assert_eq!(method, "_asyncFn");
4367                    }
4368                    _ => panic!("expected BridgeCall"),
4369                }
4370            }
4371
4372            // Promise should be pending with 1 pending promise
4373            assert_eq!(pending.len(), 1);
4374            assert!(eval_bool(&mut iso, &ctx, "_promise instanceof Promise"));
4375
4376            // Resolve the promise
4377            let result_v8 = v8_serialize_str(&mut iso, &ctx, "async result");
4378
4379            {
4380                let scope = &mut v8::HandleScope::new(&mut iso);
4381                let local = v8::Local::new(scope, &ctx);
4382                let scope = &mut v8::ContextScope::new(scope, local);
4383                bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(result_v8), None)
4384                    .unwrap();
4385            }
4386
4387            assert_eq!(pending.len(), 0);
4388
4389            // Verify promise is fulfilled with correct value
4390            {
4391                let scope = &mut v8::HandleScope::new(&mut iso);
4392                let local = v8::Local::new(scope, &ctx);
4393                let scope = &mut v8::ContextScope::new(scope, local);
4394                let source = v8::String::new(scope, "_promise").unwrap();
4395                let script = v8::Script::compile(scope, source, None).unwrap();
4396                let result = script.run(scope).unwrap();
4397                let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4398                assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4399                assert_eq!(
4400                    promise.result(scope).to_rust_string_lossy(scope),
4401                    "async result"
4402                );
4403            }
4404        }
4405
4406        // --- Part 13: Async bridge call promise rejected on error ---
4407        {
4408            let mut iso = isolate::create_isolate(None);
4409            let ctx = isolate::create_context(&mut iso);
4410
4411            let bridge_ctx = BridgeCallContext::new(
4412                Box::new(Vec::new()),
4413                Box::new(Cursor::new(Vec::new())),
4414                "test-session".into(),
4415            );
4416            let pending = bridge::PendingPromises::new();
4417
4418            let _fn_store;
4419            {
4420                let scope = &mut v8::HandleScope::new(&mut iso);
4421                let local = v8::Local::new(scope, &ctx);
4422                let scope = &mut v8::ContextScope::new(scope, local);
4423                _fn_store = bridge::register_async_bridge_fns(
4424                    scope,
4425                    &bridge_ctx as *const BridgeCallContext,
4426                    &pending as *const bridge::PendingPromises,
4427                    &["_asyncFn"],
4428                );
4429            }
4430
4431            eval(&mut iso, &ctx, "var _promise = _asyncFn('arg')");
4432            assert_eq!(pending.len(), 1);
4433
4434            // Reject the promise
4435            {
4436                let scope = &mut v8::HandleScope::new(&mut iso);
4437                let local = v8::Local::new(scope, &ctx);
4438                let scope = &mut v8::ContextScope::new(scope, local);
4439                bridge::resolve_pending_promise(
4440                    scope,
4441                    &pending,
4442                    1,
4443                    0,
4444                    None,
4445                    Some("ENOENT: file not found".into()),
4446                )
4447                .unwrap();
4448            }
4449
4450            assert_eq!(pending.len(), 0);
4451
4452            // Verify promise is rejected with error
4453            {
4454                let scope = &mut v8::HandleScope::new(&mut iso);
4455                let local = v8::Local::new(scope, &ctx);
4456                let scope = &mut v8::ContextScope::new(scope, local);
4457                let source = v8::String::new(scope, "_promise").unwrap();
4458                let script = v8::Script::compile(scope, source, None).unwrap();
4459                let result = script.run(scope).unwrap();
4460                let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4461                assert_eq!(promise.state(), v8::PromiseState::Rejected);
4462                let rejection = promise.result(scope);
4463                let obj = v8::Local::<v8::Object>::try_from(rejection).unwrap();
4464                let msg_key = v8::String::new(scope, "message").unwrap();
4465                let msg_val = obj.get(scope, msg_key.into()).unwrap();
4466                assert_eq!(
4467                    msg_val.to_rust_string_lossy(scope),
4468                    "ENOENT: file not found"
4469                );
4470            }
4471        }
4472
4473        // --- Part 14: Multiple async functions with out-of-order resolution ---
4474        {
4475            let mut iso = isolate::create_isolate(None);
4476            let ctx = isolate::create_context(&mut iso);
4477
4478            let bridge_ctx = BridgeCallContext::new(
4479                Box::new(Vec::new()),
4480                Box::new(Cursor::new(Vec::new())),
4481                "test-session".into(),
4482            );
4483            let pending = bridge::PendingPromises::new();
4484
4485            let _fn_store;
4486            {
4487                let scope = &mut v8::HandleScope::new(&mut iso);
4488                let local = v8::Local::new(scope, &ctx);
4489                let scope = &mut v8::ContextScope::new(scope, local);
4490                _fn_store = bridge::register_async_bridge_fns(
4491                    scope,
4492                    &bridge_ctx as *const BridgeCallContext,
4493                    &pending as *const bridge::PendingPromises,
4494                    &["_fetch", "_dns"],
4495                );
4496            }
4497
4498            eval(
4499                &mut iso,
4500                &ctx,
4501                "var _p1 = _fetch('url'); var _p2 = _dns('host')",
4502            );
4503            assert_eq!(pending.len(), 2);
4504
4505            // Resolve in reverse order (p2 first, then p1)
4506            let r2 = v8_serialize_str(&mut iso, &ctx, "dns-result");
4507            {
4508                let scope = &mut v8::HandleScope::new(&mut iso);
4509                let local = v8::Local::new(scope, &ctx);
4510                let scope = &mut v8::ContextScope::new(scope, local);
4511                bridge::resolve_pending_promise(scope, &pending, 2, 0, Some(r2), None).unwrap();
4512            }
4513            assert_eq!(pending.len(), 1);
4514
4515            let r1 = v8_serialize_str(&mut iso, &ctx, "fetch-result");
4516            {
4517                let scope = &mut v8::HandleScope::new(&mut iso);
4518                let local = v8::Local::new(scope, &ctx);
4519                let scope = &mut v8::ContextScope::new(scope, local);
4520                bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(r1), None).unwrap();
4521            }
4522            assert_eq!(pending.len(), 0);
4523
4524            // Verify both promises fulfilled correctly
4525            {
4526                let scope = &mut v8::HandleScope::new(&mut iso);
4527                let local = v8::Local::new(scope, &ctx);
4528                let scope = &mut v8::ContextScope::new(scope, local);
4529
4530                let source = v8::String::new(scope, "_p1").unwrap();
4531                let script = v8::Script::compile(scope, source, None).unwrap();
4532                let result = script.run(scope).unwrap();
4533                let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4534                assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4535                assert_eq!(
4536                    promise.result(scope).to_rust_string_lossy(scope),
4537                    "fetch-result"
4538                );
4539
4540                let source = v8::String::new(scope, "_p2").unwrap();
4541                let script = v8::Script::compile(scope, source, None).unwrap();
4542                let result = script.run(scope).unwrap();
4543                let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4544                assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4545                assert_eq!(
4546                    promise.result(scope).to_rust_string_lossy(scope),
4547                    "dns-result"
4548                );
4549            }
4550        }
4551
4552        // --- Part 15: Async bridge call with null result resolves to undefined ---
4553        {
4554            let mut iso = isolate::create_isolate(None);
4555            let ctx = isolate::create_context(&mut iso);
4556
4557            let bridge_ctx = BridgeCallContext::new(
4558                Box::new(Vec::new()),
4559                Box::new(Cursor::new(Vec::new())),
4560                "test-session".into(),
4561            );
4562            let pending = bridge::PendingPromises::new();
4563
4564            let _fn_store;
4565            {
4566                let scope = &mut v8::HandleScope::new(&mut iso);
4567                let local = v8::Local::new(scope, &ctx);
4568                let scope = &mut v8::ContextScope::new(scope, local);
4569                _fn_store = bridge::register_async_bridge_fns(
4570                    scope,
4571                    &bridge_ctx as *const BridgeCallContext,
4572                    &pending as *const bridge::PendingPromises,
4573                    &["_asyncFn"],
4574                );
4575            }
4576
4577            eval(&mut iso, &ctx, "var _promise = _asyncFn()");
4578
4579            // Resolve with None (null result)
4580            {
4581                let scope = &mut v8::HandleScope::new(&mut iso);
4582                let local = v8::Local::new(scope, &ctx);
4583                let scope = &mut v8::ContextScope::new(scope, local);
4584                bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap();
4585            }
4586
4587            // Promise should be fulfilled with undefined
4588            {
4589                let scope = &mut v8::HandleScope::new(&mut iso);
4590                let local = v8::Local::new(scope, &ctx);
4591                let scope = &mut v8::ContextScope::new(scope, local);
4592                let source = v8::String::new(scope, "_promise").unwrap();
4593                let script = v8::Script::compile(scope, source, None).unwrap();
4594                let result = script.run(scope).unwrap();
4595                let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4596                assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4597                assert!(promise.result(scope).is_undefined());
4598            }
4599        }
4600
4601        // --- Part 16: Microtasks flushed after promise resolution ---
4602        {
4603            let mut iso = isolate::create_isolate(None);
4604            let ctx = isolate::create_context(&mut iso);
4605
4606            let bridge_ctx = BridgeCallContext::new(
4607                Box::new(Vec::new()),
4608                Box::new(Cursor::new(Vec::new())),
4609                "test-session".into(),
4610            );
4611            let pending = bridge::PendingPromises::new();
4612
4613            let _fn_store;
4614            {
4615                let scope = &mut v8::HandleScope::new(&mut iso);
4616                let local = v8::Local::new(scope, &ctx);
4617                let scope = &mut v8::ContextScope::new(scope, local);
4618                _fn_store = bridge::register_async_bridge_fns(
4619                    scope,
4620                    &bridge_ctx as *const BridgeCallContext,
4621                    &pending as *const bridge::PendingPromises,
4622                    &["_asyncFn"],
4623                );
4624            }
4625
4626            // Set up .then handler that sets a global variable
4627            eval(
4628                &mut iso,
4629                &ctx,
4630                "var _thenRan = false; _asyncFn().then(function() { _thenRan = true; })",
4631            );
4632
4633            // Before resolution, _thenRan should be false
4634            assert!(eval_bool(&mut iso, &ctx, "_thenRan === false"));
4635
4636            // Resolve the promise (microtasks flushed inside resolve_pending_promise)
4637            {
4638                let scope = &mut v8::HandleScope::new(&mut iso);
4639                let local = v8::Local::new(scope, &ctx);
4640                let scope = &mut v8::ContextScope::new(scope, local);
4641                bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap();
4642            }
4643
4644            // After resolution + microtask flush, _thenRan should be true
4645            assert!(eval_bool(&mut iso, &ctx, "_thenRan === true"));
4646        }
4647
4648        // --- Part 17: CJS execution — successful execution returns exit code 0 ---
4649        {
4650            let mut iso = isolate::create_isolate(None);
4651            let ctx = isolate::create_context(&mut iso);
4652
4653            let (code, error) = {
4654                let scope = &mut v8::HandleScope::new(&mut iso);
4655                let local = v8::Local::new(scope, &ctx);
4656                let scope = &mut v8::ContextScope::new(scope, local);
4657                execute_script(scope, "", "var x = 1 + 2;", &mut None)
4658            };
4659
4660            assert_eq!(code, 0);
4661            assert!(error.is_none());
4662            // Verify the code actually ran
4663            assert_eq!(eval(&mut iso, &ctx, "x"), "3");
4664        }
4665
4666        // --- Part 18: Bridge code IIFE executed before user code ---
4667        {
4668            let mut iso = isolate::create_isolate(None);
4669            let ctx = isolate::create_context(&mut iso);
4670
4671            let bridge = "(function() { globalThis._bridgeReady = true; })()";
4672            let user = "var _sawBridge = _bridgeReady;";
4673            let (code, error) = {
4674                let scope = &mut v8::HandleScope::new(&mut iso);
4675                let local = v8::Local::new(scope, &ctx);
4676                let scope = &mut v8::ContextScope::new(scope, local);
4677                execute_script(scope, bridge, user, &mut None)
4678            };
4679
4680            assert_eq!(code, 0);
4681            assert!(error.is_none());
4682            assert!(eval_bool(&mut iso, &ctx, "_sawBridge === true"));
4683            assert!(eval_bool(&mut iso, &ctx, "_bridgeReady === true"));
4684        }
4685
4686        // --- Part 18b: Rejected async script completion returns structured error ---
4687        {
4688            let mut iso = isolate::create_isolate(None);
4689            let ctx = isolate::create_context(&mut iso);
4690
4691            let (code, error) = {
4692                let scope = &mut v8::HandleScope::new(&mut iso);
4693                let local = v8::Local::new(scope, &ctx);
4694                let scope = &mut v8::ContextScope::new(scope, local);
4695                execute_script(
4696                    scope,
4697                    "",
4698                    "(async function () { throw new Error('async failure'); })()",
4699                    &mut None,
4700                )
4701            };
4702
4703            assert_eq!(code, 1);
4704            let err = error.unwrap();
4705            assert_eq!(err.error_type, "Error");
4706            assert_eq!(err.message, "async failure");
4707        }
4708
4709        // --- Part 19: SyntaxError in user code returns structured error ---
4710        {
4711            let mut iso = isolate::create_isolate(None);
4712            let ctx = isolate::create_context(&mut iso);
4713
4714            let (code, error) = {
4715                let scope = &mut v8::HandleScope::new(&mut iso);
4716                let local = v8::Local::new(scope, &ctx);
4717                let scope = &mut v8::ContextScope::new(scope, local);
4718                execute_script(scope, "", "var x = {;", &mut None)
4719            };
4720
4721            assert_eq!(code, 1);
4722            let err = error.unwrap();
4723            assert_eq!(err.error_type, "SyntaxError");
4724            assert!(!err.message.is_empty());
4725        }
4726
4727        // --- Part 20: Runtime TypeError returns structured error ---
4728        {
4729            let mut iso = isolate::create_isolate(None);
4730            let ctx = isolate::create_context(&mut iso);
4731
4732            let (code, error) = {
4733                let scope = &mut v8::HandleScope::new(&mut iso);
4734                let local = v8::Local::new(scope, &ctx);
4735                let scope = &mut v8::ContextScope::new(scope, local);
4736                execute_script(scope, "", "null.foo", &mut None)
4737            };
4738
4739            assert_eq!(code, 1);
4740            let err = error.unwrap();
4741            assert_eq!(err.error_type, "TypeError");
4742            assert!(!err.message.is_empty());
4743            assert!(!err.stack.is_empty());
4744        }
4745
4746        // --- Part 21: SyntaxError in bridge code returns error ---
4747        {
4748            let mut iso = isolate::create_isolate(None);
4749            let ctx = isolate::create_context(&mut iso);
4750
4751            let (code, error) = {
4752                let scope = &mut v8::HandleScope::new(&mut iso);
4753                let local = v8::Local::new(scope, &ctx);
4754                let scope = &mut v8::ContextScope::new(scope, local);
4755                execute_script(scope, "function {", "var x = 1;", &mut None)
4756            };
4757
4758            assert_eq!(code, 1);
4759            let err = error.unwrap();
4760            assert_eq!(err.error_type, "SyntaxError");
4761            // User code should NOT have run
4762            assert!(eval_bool(&mut iso, &ctx, "typeof x === 'undefined'"));
4763        }
4764
4765        // --- Part 22: Empty bridge code is skipped ---
4766        {
4767            let mut iso = isolate::create_isolate(None);
4768            let ctx = isolate::create_context(&mut iso);
4769
4770            let (code, error) = {
4771                let scope = &mut v8::HandleScope::new(&mut iso);
4772                let local = v8::Local::new(scope, &ctx);
4773                let scope = &mut v8::ContextScope::new(scope, local);
4774                execute_script(scope, "", "'hello'", &mut None)
4775            };
4776
4777            assert_eq!(code, 0);
4778            assert!(error.is_none());
4779        }
4780
4781        // --- Part 23: Runtime error with error code ---
4782        {
4783            let mut iso = isolate::create_isolate(None);
4784            let ctx = isolate::create_context(&mut iso);
4785
4786            let (code, error) = {
4787                let scope = &mut v8::HandleScope::new(&mut iso);
4788                let local = v8::Local::new(scope, &ctx);
4789                let scope = &mut v8::ContextScope::new(scope, local);
4790                execute_script(
4791                    scope,
4792                    "",
4793                    "var e = new Error('not found'); e.code = 'ERR_MODULE_NOT_FOUND'; throw e;",
4794                    &mut None,
4795                )
4796            };
4797
4798            assert_eq!(code, 1);
4799            let err = error.unwrap();
4800            assert_eq!(err.error_type, "Error");
4801            assert_eq!(err.message, "not found");
4802            assert_eq!(err.code, Some("ERR_MODULE_NOT_FOUND".into()));
4803        }
4804
4805        // --- Part 24: Thrown string (non-Error object) handled ---
4806        {
4807            let mut iso = isolate::create_isolate(None);
4808            let ctx = isolate::create_context(&mut iso);
4809
4810            let (code, error) = {
4811                let scope = &mut v8::HandleScope::new(&mut iso);
4812                let local = v8::Local::new(scope, &ctx);
4813                let scope = &mut v8::ContextScope::new(scope, local);
4814                execute_script(scope, "", "throw 'raw string error';", &mut None)
4815            };
4816
4817            assert_eq!(code, 1);
4818            let err = error.unwrap();
4819            assert_eq!(err.error_type, "Error");
4820            assert_eq!(err.message, "raw string error");
4821            assert!(err.stack.is_empty());
4822            assert!(err.code.is_none());
4823        }
4824
4825        // --- Part 25: ESM — simple module with exports ---
4826        {
4827            let mut iso = isolate::create_isolate(None);
4828            let ctx = isolate::create_context(&mut iso);
4829
4830            let bridge_ctx = BridgeCallContext::new(
4831                Box::new(Vec::new()),
4832                Box::new(Cursor::new(Vec::new())),
4833                "test-session".into(),
4834            );
4835
4836            let user_code = "export const x = 42;\nexport const msg = 'hello';";
4837            let (code, exports, error) = {
4838                let scope = &mut v8::HandleScope::new(&mut iso);
4839                let local = v8::Local::new(scope, &ctx);
4840                let scope = &mut v8::ContextScope::new(scope, local);
4841                execute_module(scope, &bridge_ctx, "", user_code, None, &mut None)
4842            };
4843
4844            assert_eq!(code, 0);
4845            assert!(error.is_none());
4846            let exports = exports.unwrap();
4847            {
4848                let scope = &mut v8::HandleScope::new(&mut iso);
4849                let local = v8::Local::new(scope, &ctx);
4850                let scope = &mut v8::ContextScope::new(scope, local);
4851                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4852                assert!(val.is_object());
4853                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4854                let k = v8::String::new(scope, "x").unwrap();
4855                assert_eq!(
4856                    obj.get(scope, k.into())
4857                        .unwrap()
4858                        .int32_value(scope)
4859                        .unwrap(),
4860                    42
4861                );
4862                let k = v8::String::new(scope, "msg").unwrap();
4863                assert_eq!(
4864                    obj.get(scope, k.into())
4865                        .unwrap()
4866                        .to_rust_string_lossy(scope),
4867                    "hello"
4868                );
4869            }
4870        }
4871
4872        // --- Part 25a: ESM root modules receive fetch globals from the runtime prelude ---
4873        {
4874            let mut iso = isolate::create_isolate(None);
4875            let ctx = isolate::create_context(&mut iso);
4876
4877            let bridge_ctx = BridgeCallContext::new(
4878                Box::new(Vec::new()),
4879                Box::new(Cursor::new(Vec::new())),
4880                "test-session".into(),
4881            );
4882
4883            let bridge_code = r#"
4884                globalThis.fetch = async function () { return "ok"; };
4885            "#;
4886            let user_code = r#"
4887                const result = await fetch();
4888                export const fetchType = typeof fetch;
4889                export default result;
4890            "#;
4891            let (code, exports, error) = {
4892                let scope = &mut v8::HandleScope::new(&mut iso);
4893                let local = v8::Local::new(scope, &ctx);
4894                let scope = &mut v8::ContextScope::new(scope, local);
4895                execute_module(scope, &bridge_ctx, bridge_code, user_code, None, &mut None)
4896            };
4897
4898            assert_eq!(code, 0, "error: {:?}", error);
4899            assert!(error.is_none());
4900            let exports = exports.unwrap();
4901            {
4902                let scope = &mut v8::HandleScope::new(&mut iso);
4903                let local = v8::Local::new(scope, &ctx);
4904                let scope = &mut v8::ContextScope::new(scope, local);
4905                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4906                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4907
4908                let fetch_type_key = v8::String::new(scope, "fetchType").unwrap();
4909                assert_eq!(
4910                    obj.get(scope, fetch_type_key.into())
4911                        .unwrap()
4912                        .to_rust_string_lossy(scope),
4913                    "function"
4914                );
4915
4916                let default_key = v8::String::new(scope, "default").unwrap();
4917                assert_eq!(
4918                    obj.get(scope, default_key.into())
4919                        .unwrap()
4920                        .to_rust_string_lossy(scope),
4921                    "ok"
4922                );
4923            }
4924        }
4925
4926        // --- Part 26: ESM — default export ---
4927        {
4928            let mut iso = isolate::create_isolate(None);
4929            let ctx = isolate::create_context(&mut iso);
4930
4931            let bridge_ctx = BridgeCallContext::new(
4932                Box::new(Vec::new()),
4933                Box::new(Cursor::new(Vec::new())),
4934                "test-session".into(),
4935            );
4936
4937            let (code, exports, error) = {
4938                let scope = &mut v8::HandleScope::new(&mut iso);
4939                let local = v8::Local::new(scope, &ctx);
4940                let scope = &mut v8::ContextScope::new(scope, local);
4941                execute_module(
4942                    scope,
4943                    &bridge_ctx,
4944                    "",
4945                    "export default 'world';",
4946                    None,
4947                    &mut None,
4948                )
4949            };
4950
4951            assert_eq!(code, 0);
4952            assert!(error.is_none());
4953            let exports = exports.unwrap();
4954            {
4955                let scope = &mut v8::HandleScope::new(&mut iso);
4956                let local = v8::Local::new(scope, &ctx);
4957                let scope = &mut v8::ContextScope::new(scope, local);
4958                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4959                assert!(val.is_object());
4960                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4961                let k = v8::String::new(scope, "default").unwrap();
4962                assert_eq!(
4963                    obj.get(scope, k.into())
4964                        .unwrap()
4965                        .to_rust_string_lossy(scope),
4966                    "world"
4967                );
4968            }
4969        }
4970
4971        // --- Part 27: ESM — SyntaxError ---
4972        {
4973            let mut iso = isolate::create_isolate(None);
4974            let ctx = isolate::create_context(&mut iso);
4975
4976            let bridge_ctx = BridgeCallContext::new(
4977                Box::new(Vec::new()),
4978                Box::new(Cursor::new(Vec::new())),
4979                "test-session".into(),
4980            );
4981
4982            let (code, _exports, error) = {
4983                let scope = &mut v8::HandleScope::new(&mut iso);
4984                let local = v8::Local::new(scope, &ctx);
4985                let scope = &mut v8::ContextScope::new(scope, local);
4986                execute_module(
4987                    scope,
4988                    &bridge_ctx,
4989                    "",
4990                    "export const x = {;",
4991                    None,
4992                    &mut None,
4993                )
4994            };
4995
4996            assert_eq!(code, 1);
4997            let err = error.unwrap();
4998            assert_eq!(err.error_type, "SyntaxError");
4999        }
5000
5001        // --- Part 28: ESM — runtime TypeError ---
5002        {
5003            let mut iso = isolate::create_isolate(None);
5004            let ctx = isolate::create_context(&mut iso);
5005
5006            let bridge_ctx = BridgeCallContext::new(
5007                Box::new(Vec::new()),
5008                Box::new(Cursor::new(Vec::new())),
5009                "test-session".into(),
5010            );
5011
5012            let (code, _exports, error) = {
5013                let scope = &mut v8::HandleScope::new(&mut iso);
5014                let local = v8::Local::new(scope, &ctx);
5015                let scope = &mut v8::ContextScope::new(scope, local);
5016                execute_module(
5017                    scope,
5018                    &bridge_ctx,
5019                    "",
5020                    "const x = null; x.foo;",
5021                    None,
5022                    &mut None,
5023                )
5024            };
5025
5026            assert_eq!(code, 1);
5027            let err = error.unwrap();
5028            assert_eq!(err.error_type, "TypeError");
5029        }
5030
5031        // --- Part 29: ESM — bridge code IIFE runs before module ---
5032        {
5033            let mut iso = isolate::create_isolate(None);
5034            let ctx = isolate::create_context(&mut iso);
5035
5036            let bridge_ctx = BridgeCallContext::new(
5037                Box::new(Vec::new()),
5038                Box::new(Cursor::new(Vec::new())),
5039                "test-session".into(),
5040            );
5041
5042            let bridge = "(function() { globalThis._bridgeReady = true; })()";
5043            let user = "export const saw = _bridgeReady;";
5044            let (code, exports, error) = {
5045                let scope = &mut v8::HandleScope::new(&mut iso);
5046                let local = v8::Local::new(scope, &ctx);
5047                let scope = &mut v8::ContextScope::new(scope, local);
5048                execute_module(scope, &bridge_ctx, bridge, user, None, &mut None)
5049            };
5050
5051            assert_eq!(code, 0);
5052            assert!(error.is_none());
5053            let exports = exports.unwrap();
5054            {
5055                let scope = &mut v8::HandleScope::new(&mut iso);
5056                let local = v8::Local::new(scope, &ctx);
5057                let scope = &mut v8::ContextScope::new(scope, local);
5058                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
5059                assert!(val.is_object());
5060                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
5061                let k = v8::String::new(scope, "saw").unwrap();
5062                assert!(obj.get(scope, k.into()).unwrap().is_true());
5063            }
5064        }
5065
5066        // --- Part 30: ESM — import from dependency via batch resolve ---
5067        {
5068            let mut iso = isolate::create_isolate(None);
5069            let ctx = isolate::create_context(&mut iso);
5070
5071            // Prepare BridgeResponse for _batchResolveModules (batch prefetch).
5072            // The batch call (call_id=1) returns an array of {resolved, source}.
5073            let mut response_buf = Vec::new();
5074
5075            let batch_result = v8_serialize_eval(
5076                &mut iso,
5077                &ctx,
5078                "[{resolved: '/dep.mjs', source: 'export const dep_val = 99;'}]",
5079            );
5080            crate::ipc_binary::write_frame(
5081                &mut response_buf,
5082                &crate::ipc_binary::BinaryFrame::BridgeResponse {
5083                    session_id: String::new(),
5084                    call_id: 1,
5085                    status: 0,
5086                    payload: batch_result,
5087                },
5088            )
5089            .unwrap();
5090            crate::ipc_binary::write_frame(
5091                &mut response_buf,
5092                &crate::ipc_binary::BinaryFrame::BridgeResponse {
5093                    session_id: String::new(),
5094                    call_id: 2,
5095                    status: 0,
5096                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
5097                },
5098            )
5099            .unwrap();
5100            crate::ipc_binary::write_frame(
5101                &mut response_buf,
5102                &crate::ipc_binary::BinaryFrame::BridgeResponse {
5103                    session_id: String::new(),
5104                    call_id: 3,
5105                    status: 0,
5106                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
5107                },
5108            )
5109            .unwrap();
5110            crate::ipc_binary::write_frame(
5111                &mut response_buf,
5112                &crate::ipc_binary::BinaryFrame::BridgeResponse {
5113                    session_id: String::new(),
5114                    call_id: 2,
5115                    status: 0,
5116                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
5117                },
5118            )
5119            .unwrap();
5120
5121            let bridge_ctx = BridgeCallContext::new(
5122                Box::new(Vec::new()),
5123                Box::new(Cursor::new(response_buf)),
5124                "test-session".into(),
5125            );
5126
5127            let user_code =
5128                "import { dep_val } from './dep.mjs';\nexport const result = dep_val + 1;";
5129            let (code, exports, error) = {
5130                let scope = &mut v8::HandleScope::new(&mut iso);
5131                let local = v8::Local::new(scope, &ctx);
5132                let scope = &mut v8::ContextScope::new(scope, local);
5133                execute_module(
5134                    scope,
5135                    &bridge_ctx,
5136                    "",
5137                    user_code,
5138                    Some("/app/main.mjs"),
5139                    &mut None,
5140                )
5141            };
5142
5143            assert_eq!(code, 0, "error: {:?}", error);
5144            assert!(error.is_none());
5145            let exports = exports.unwrap();
5146            {
5147                let scope = &mut v8::HandleScope::new(&mut iso);
5148                let local = v8::Local::new(scope, &ctx);
5149                let scope = &mut v8::ContextScope::new(scope, local);
5150                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
5151                assert!(val.is_object());
5152                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
5153                let k = v8::String::new(scope, "result").unwrap();
5154                assert_eq!(
5155                    obj.get(scope, k.into())
5156                        .unwrap()
5157                        .int32_value(scope)
5158                        .unwrap(),
5159                    100
5160                );
5161            }
5162        }
5163
5164        // --- Part 31: Event loop — BridgeResponse resolves pending promise ---
5165        {
5166            let mut iso = isolate::create_isolate(None);
5167            let ctx = isolate::create_context(&mut iso);
5168
5169            let bridge_ctx = BridgeCallContext::new(
5170                Box::new(Vec::new()),
5171                Box::new(Cursor::new(Vec::new())),
5172                "test-session".into(),
5173            );
5174            let pending = bridge::PendingPromises::new();
5175
5176            // Register async bridge function
5177            let _fn_store;
5178            {
5179                let scope = &mut v8::HandleScope::new(&mut iso);
5180                let local = v8::Local::new(scope, &ctx);
5181                let scope = &mut v8::ContextScope::new(scope, local);
5182                _fn_store = bridge::register_async_bridge_fns(
5183                    scope,
5184                    &bridge_ctx as *const BridgeCallContext,
5185                    &pending as *const bridge::PendingPromises,
5186                    &["_asyncFn"],
5187                );
5188            }
5189
5190            // Call async function from V8 — creates pending promise
5191            eval(
5192                &mut iso,
5193                &ctx,
5194                "var _eventLoopResult = 'pending'; _asyncFn('test').then(function(v) { _eventLoopResult = v; })",
5195            );
5196            assert_eq!(pending.len(), 1);
5197            assert_eq!(eval(&mut iso, &ctx, "_eventLoopResult"), "pending");
5198
5199            // Create channel and send BridgeResponse
5200            let (tx, rx) = crossbeam_channel::unbounded();
5201            let result_v8 = v8_serialize_str(&mut iso, &ctx, "event-loop-resolved");
5202            tx.send(crate::session::SessionCommand::Message(
5203                crate::runtime_protocol::SessionMessage::BridgeResponse(
5204                    crate::runtime_protocol::BridgeResponse {
5205                        call_id: 1,
5206                        status: 0,
5207                        payload: result_v8,
5208                    },
5209                ),
5210            ))
5211            .unwrap();
5212
5213            // Run event loop
5214            let completed = {
5215                let scope = &mut v8::HandleScope::new(&mut iso);
5216                let local = v8::Local::new(scope, &ctx);
5217                let scope = &mut v8::ContextScope::new(scope, local);
5218                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5219            };
5220
5221            assert!(
5222                matches!(completed, crate::session::EventLoopStatus::Completed),
5223                "event loop should complete normally"
5224            );
5225            assert_eq!(pending.len(), 0);
5226            assert_eq!(
5227                eval(&mut iso, &ctx, "_eventLoopResult"),
5228                "event-loop-resolved"
5229            );
5230        }
5231
5232        // --- Part 32: Event loop — multiple BridgeResponses resolved in sequence ---
5233        {
5234            let mut iso = isolate::create_isolate(None);
5235            let ctx = isolate::create_context(&mut iso);
5236
5237            let bridge_ctx = BridgeCallContext::new(
5238                Box::new(Vec::new()),
5239                Box::new(Cursor::new(Vec::new())),
5240                "test-session".into(),
5241            );
5242            let pending = bridge::PendingPromises::new();
5243
5244            let _fn_store;
5245            {
5246                let scope = &mut v8::HandleScope::new(&mut iso);
5247                let local = v8::Local::new(scope, &ctx);
5248                let scope = &mut v8::ContextScope::new(scope, local);
5249                _fn_store = bridge::register_async_bridge_fns(
5250                    scope,
5251                    &bridge_ctx as *const BridgeCallContext,
5252                    &pending as *const bridge::PendingPromises,
5253                    &["_fetch", "_dns"],
5254                );
5255            }
5256
5257            // Create two pending promises
5258            eval(
5259                &mut iso,
5260                &ctx,
5261                "var _r1 = 'pending'; var _r2 = 'pending'; \
5262                 _fetch('url').then(function(v) { _r1 = v; }); \
5263                 _dns('host').then(function(v) { _r2 = v; })",
5264            );
5265            assert_eq!(pending.len(), 2);
5266
5267            // Create channel and send both responses
5268            let (tx, rx) = crossbeam_channel::unbounded();
5269            // Resolve in reverse order
5270            let r2 = v8_serialize_str(&mut iso, &ctx, "dns-result");
5271            tx.send(crate::session::SessionCommand::Message(
5272                crate::runtime_protocol::SessionMessage::BridgeResponse(
5273                    crate::runtime_protocol::BridgeResponse {
5274                        call_id: 2,
5275                        status: 0,
5276                        payload: r2,
5277                    },
5278                ),
5279            ))
5280            .unwrap();
5281            let r1 = v8_serialize_str(&mut iso, &ctx, "fetch-result");
5282            tx.send(crate::session::SessionCommand::Message(
5283                crate::runtime_protocol::SessionMessage::BridgeResponse(
5284                    crate::runtime_protocol::BridgeResponse {
5285                        call_id: 1,
5286                        status: 0,
5287                        payload: r1,
5288                    },
5289                ),
5290            ))
5291            .unwrap();
5292
5293            let completed = {
5294                let scope = &mut v8::HandleScope::new(&mut iso);
5295                let local = v8::Local::new(scope, &ctx);
5296                let scope = &mut v8::ContextScope::new(scope, local);
5297                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5298            };
5299
5300            assert!(matches!(
5301                completed,
5302                crate::session::EventLoopStatus::Completed
5303            ));
5304            assert_eq!(pending.len(), 0);
5305            assert_eq!(eval(&mut iso, &ctx, "_r1"), "fetch-result");
5306            assert_eq!(eval(&mut iso, &ctx, "_r2"), "dns-result");
5307        }
5308
5309        // --- Part 33: Event loop — TerminateExecution breaks loop ---
5310        {
5311            let mut iso = isolate::create_isolate(None);
5312            let ctx = isolate::create_context(&mut iso);
5313
5314            let bridge_ctx = BridgeCallContext::new(
5315                Box::new(Vec::new()),
5316                Box::new(Cursor::new(Vec::new())),
5317                "test-session".into(),
5318            );
5319            let pending = bridge::PendingPromises::new();
5320
5321            let _fn_store;
5322            {
5323                let scope = &mut v8::HandleScope::new(&mut iso);
5324                let local = v8::Local::new(scope, &ctx);
5325                let scope = &mut v8::ContextScope::new(scope, local);
5326                _fn_store = bridge::register_async_bridge_fns(
5327                    scope,
5328                    &bridge_ctx as *const BridgeCallContext,
5329                    &pending as *const bridge::PendingPromises,
5330                    &["_asyncFn"],
5331                );
5332            }
5333
5334            eval(&mut iso, &ctx, "_asyncFn('test')");
5335            assert_eq!(pending.len(), 1);
5336
5337            // Send TerminateExecution
5338            let (tx, rx) = crossbeam_channel::unbounded();
5339            tx.send(crate::session::SessionCommand::Message(
5340                crate::runtime_protocol::SessionMessage::TerminateExecution,
5341            ))
5342            .unwrap();
5343
5344            let completed = {
5345                let scope = &mut v8::HandleScope::new(&mut iso);
5346                let local = v8::Local::new(scope, &ctx);
5347                let scope = &mut v8::ContextScope::new(scope, local);
5348                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5349            };
5350
5351            assert!(
5352                matches!(completed, crate::session::EventLoopStatus::Terminated),
5353                "event loop should return terminated status on termination"
5354            );
5355            // Promise is still pending (not resolved)
5356            assert_eq!(pending.len(), 1);
5357
5358            // Cancel termination so isolate is usable again
5359            iso.cancel_terminate_execution();
5360        }
5361
5362        // --- Part 34: Event loop — Shutdown breaks loop ---
5363        {
5364            let mut iso = isolate::create_isolate(None);
5365            let ctx = isolate::create_context(&mut iso);
5366
5367            let bridge_ctx = BridgeCallContext::new(
5368                Box::new(Vec::new()),
5369                Box::new(Cursor::new(Vec::new())),
5370                "test-session".into(),
5371            );
5372            let pending = bridge::PendingPromises::new();
5373
5374            let _fn_store;
5375            {
5376                let scope = &mut v8::HandleScope::new(&mut iso);
5377                let local = v8::Local::new(scope, &ctx);
5378                let scope = &mut v8::ContextScope::new(scope, local);
5379                _fn_store = bridge::register_async_bridge_fns(
5380                    scope,
5381                    &bridge_ctx as *const BridgeCallContext,
5382                    &pending as *const bridge::PendingPromises,
5383                    &["_asyncFn"],
5384                );
5385            }
5386
5387            eval(&mut iso, &ctx, "_asyncFn('test')");
5388            assert_eq!(pending.len(), 1);
5389
5390            // Send Shutdown
5391            let (tx, rx) = crossbeam_channel::unbounded();
5392            tx.send(crate::session::SessionCommand::Shutdown).unwrap();
5393
5394            let completed = {
5395                let scope = &mut v8::HandleScope::new(&mut iso);
5396                let local = v8::Local::new(scope, &ctx);
5397                let scope = &mut v8::ContextScope::new(scope, local);
5398                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5399            };
5400
5401            assert!(
5402                matches!(completed, crate::session::EventLoopStatus::Terminated),
5403                "event loop should return terminated status on shutdown"
5404            );
5405        }
5406
5407        // --- Part 35: Event loop — exits immediately when no pending promises ---
5408        {
5409            let mut iso = isolate::create_isolate(None);
5410            let ctx = isolate::create_context(&mut iso);
5411            let pending = bridge::PendingPromises::new();
5412
5413            let (_tx, rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
5414
5415            // No pending promises — event loop should exit immediately
5416            let completed = {
5417                let scope = &mut v8::HandleScope::new(&mut iso);
5418                let local = v8::Local::new(scope, &ctx);
5419                let scope = &mut v8::ContextScope::new(scope, local);
5420                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5421            };
5422
5423            assert!(matches!(
5424                completed,
5425                crate::session::EventLoopStatus::Completed
5426            ));
5427        }
5428
5429        // --- Part 36: Event loop — StreamEvent dispatches to V8 callback ---
5430        {
5431            let mut iso = isolate::create_isolate(None);
5432            let ctx = isolate::create_context(&mut iso);
5433
5434            let bridge_ctx = BridgeCallContext::new(
5435                Box::new(Vec::new()),
5436                Box::new(Cursor::new(Vec::new())),
5437                "test-session".into(),
5438            );
5439            let pending = bridge::PendingPromises::new();
5440
5441            let _fn_store;
5442            {
5443                let scope = &mut v8::HandleScope::new(&mut iso);
5444                let local = v8::Local::new(scope, &ctx);
5445                let scope = &mut v8::ContextScope::new(scope, local);
5446                _fn_store = bridge::register_async_bridge_fns(
5447                    scope,
5448                    &bridge_ctx as *const BridgeCallContext,
5449                    &pending as *const bridge::PendingPromises,
5450                    &["_asyncFn"],
5451                );
5452            }
5453
5454            // Register dispatch callback and create pending promise
5455            eval(
5456                &mut iso,
5457                &ctx,
5458                "var _streamEvents = []; \
5459                 globalThis._childProcessDispatch = function(eventType, payload) { \
5460                     _streamEvents.push({ type: eventType, data: payload }); \
5461                 }; \
5462                 _asyncFn('keep-alive')",
5463            );
5464            assert_eq!(pending.len(), 1);
5465
5466            // Send StreamEvent followed by BridgeResponse
5467            let (tx, rx) = crossbeam_channel::unbounded();
5468
5469            // Encode payload as V8-serialized string
5470            let payload_bytes = v8_serialize_str(&mut iso, &ctx, "hello from child");
5471
5472            tx.send(crate::session::SessionCommand::Message(
5473                crate::runtime_protocol::SessionMessage::StreamEvent(
5474                    crate::runtime_protocol::StreamEvent {
5475                        event_type: "child_stdout".into(),
5476                        payload: payload_bytes,
5477                    },
5478                ),
5479            ))
5480            .unwrap();
5481
5482            // Resolve the pending promise to exit the event loop
5483            let r = v8_serialize_null(&mut iso, &ctx);
5484            tx.send(crate::session::SessionCommand::Message(
5485                crate::runtime_protocol::SessionMessage::BridgeResponse(
5486                    crate::runtime_protocol::BridgeResponse {
5487                        call_id: 1,
5488                        status: 0,
5489                        payload: r,
5490                    },
5491                ),
5492            ))
5493            .unwrap();
5494
5495            let completed = {
5496                let scope = &mut v8::HandleScope::new(&mut iso);
5497                let local = v8::Local::new(scope, &ctx);
5498                let scope = &mut v8::ContextScope::new(scope, local);
5499                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5500            };
5501
5502            assert!(matches!(
5503                completed,
5504                crate::session::EventLoopStatus::Completed
5505            ));
5506            assert_eq!(pending.len(), 0);
5507
5508            // Verify stream event was dispatched
5509            assert_eq!(eval(&mut iso, &ctx, "_streamEvents.length"), "1");
5510            assert_eq!(
5511                eval(&mut iso, &ctx, "_streamEvents[0].type"),
5512                "child_stdout"
5513            );
5514            assert_eq!(
5515                eval(&mut iso, &ctx, "_streamEvents[0].data"),
5516                "hello from child"
5517            );
5518        }
5519
5520        // --- Part 37: Event loop — microtasks flushed after BridgeResponse ---
5521        {
5522            let mut iso = isolate::create_isolate(None);
5523            let ctx = isolate::create_context(&mut iso);
5524
5525            let bridge_ctx = BridgeCallContext::new(
5526                Box::new(Vec::new()),
5527                Box::new(Cursor::new(Vec::new())),
5528                "test-session".into(),
5529            );
5530            let pending = bridge::PendingPromises::new();
5531
5532            let _fn_store;
5533            {
5534                let scope = &mut v8::HandleScope::new(&mut iso);
5535                let local = v8::Local::new(scope, &ctx);
5536                let scope = &mut v8::ContextScope::new(scope, local);
5537                _fn_store = bridge::register_async_bridge_fns(
5538                    scope,
5539                    &bridge_ctx as *const BridgeCallContext,
5540                    &pending as *const bridge::PendingPromises,
5541                    &["_asyncFn"],
5542                );
5543            }
5544
5545            // Set up .then handler that mutates global state
5546            eval(
5547                &mut iso,
5548                &ctx,
5549                "var _microtaskRan = false; \
5550                 _asyncFn('test').then(function() { _microtaskRan = true; })",
5551            );
5552            assert!(eval_bool(&mut iso, &ctx, "_microtaskRan === false"));
5553
5554            let (tx, rx) = crossbeam_channel::unbounded();
5555            let r = v8_serialize_null(&mut iso, &ctx);
5556            tx.send(crate::session::SessionCommand::Message(
5557                crate::runtime_protocol::SessionMessage::BridgeResponse(
5558                    crate::runtime_protocol::BridgeResponse {
5559                        call_id: 1,
5560                        status: 0,
5561                        payload: r,
5562                    },
5563                ),
5564            ))
5565            .unwrap();
5566
5567            {
5568                let scope = &mut v8::HandleScope::new(&mut iso);
5569                let local = v8::Local::new(scope, &ctx);
5570                let scope = &mut v8::ContextScope::new(scope, local);
5571                crate::session::run_event_loop(scope, &rx, &pending, None, None);
5572            }
5573
5574            // .then handler should have run (microtasks flushed)
5575            assert!(eval_bool(&mut iso, &ctx, "_microtaskRan === true"));
5576        }
5577
5578        // --- Part 38: StreamEvent dispatches child_stderr and child_exit ---
5579        {
5580            let mut iso = isolate::create_isolate(None);
5581            let ctx = isolate::create_context(&mut iso);
5582
5583            let bridge_ctx = BridgeCallContext::new(
5584                Box::new(Vec::new()),
5585                Box::new(Cursor::new(Vec::new())),
5586                "test-session".into(),
5587            );
5588            let pending = bridge::PendingPromises::new();
5589
5590            let _fn_store;
5591            {
5592                let scope = &mut v8::HandleScope::new(&mut iso);
5593                let local = v8::Local::new(scope, &ctx);
5594                let scope = &mut v8::ContextScope::new(scope, local);
5595                _fn_store = bridge::register_async_bridge_fns(
5596                    scope,
5597                    &bridge_ctx as *const BridgeCallContext,
5598                    &pending as *const bridge::PendingPromises,
5599                    &["_asyncFn"],
5600                );
5601            }
5602
5603            // Register child process dispatch and create pending promise
5604            eval(
5605                &mut iso,
5606                &ctx,
5607                "var _childEvents = []; \
5608                 globalThis._childProcessDispatch = function(eventType, payload) { \
5609                     _childEvents.push({ type: eventType, data: payload }); \
5610                 }; \
5611                 _asyncFn('keep-alive')",
5612            );
5613            assert_eq!(pending.len(), 1);
5614
5615            let (tx, rx) = crossbeam_channel::unbounded();
5616
5617            // Send child_stderr event
5618            let stderr_payload = v8_serialize_str(&mut iso, &ctx, "error output");
5619            tx.send(crate::session::SessionCommand::Message(
5620                crate::runtime_protocol::SessionMessage::StreamEvent(
5621                    crate::runtime_protocol::StreamEvent {
5622                        event_type: "child_stderr".into(),
5623                        payload: stderr_payload,
5624                    },
5625                ),
5626            ))
5627            .unwrap();
5628
5629            // Send child_exit event with exit code
5630            let exit_payload = v8_serialize_int(&mut iso, &ctx, 1);
5631            tx.send(crate::session::SessionCommand::Message(
5632                crate::runtime_protocol::SessionMessage::StreamEvent(
5633                    crate::runtime_protocol::StreamEvent {
5634                        event_type: "child_exit".into(),
5635                        payload: exit_payload,
5636                    },
5637                ),
5638            ))
5639            .unwrap();
5640
5641            // Resolve the pending promise to exit the event loop
5642            let r = v8_serialize_null(&mut iso, &ctx);
5643            tx.send(crate::session::SessionCommand::Message(
5644                crate::runtime_protocol::SessionMessage::BridgeResponse(
5645                    crate::runtime_protocol::BridgeResponse {
5646                        call_id: 1,
5647                        status: 0,
5648                        payload: r,
5649                    },
5650                ),
5651            ))
5652            .unwrap();
5653
5654            let completed = {
5655                let scope = &mut v8::HandleScope::new(&mut iso);
5656                let local = v8::Local::new(scope, &ctx);
5657                let scope = &mut v8::ContextScope::new(scope, local);
5658                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5659            };
5660
5661            assert!(matches!(
5662                completed,
5663                crate::session::EventLoopStatus::Completed
5664            ));
5665            assert_eq!(eval(&mut iso, &ctx, "_childEvents.length"), "2");
5666            assert_eq!(eval(&mut iso, &ctx, "_childEvents[0].type"), "child_stderr");
5667            assert_eq!(eval(&mut iso, &ctx, "_childEvents[0].data"), "error output");
5668            assert_eq!(eval(&mut iso, &ctx, "_childEvents[1].type"), "child_exit");
5669            assert_eq!(eval(&mut iso, &ctx, "_childEvents[1].data"), "1");
5670        }
5671
5672        // --- Part 39: StreamEvent dispatches http_request to _httpServerDispatch ---
5673        {
5674            let mut iso = isolate::create_isolate(None);
5675            let ctx = isolate::create_context(&mut iso);
5676
5677            let bridge_ctx = BridgeCallContext::new(
5678                Box::new(Vec::new()),
5679                Box::new(Cursor::new(Vec::new())),
5680                "test-session".into(),
5681            );
5682            let pending = bridge::PendingPromises::new();
5683
5684            let _fn_store;
5685            {
5686                let scope = &mut v8::HandleScope::new(&mut iso);
5687                let local = v8::Local::new(scope, &ctx);
5688                let scope = &mut v8::ContextScope::new(scope, local);
5689                _fn_store = bridge::register_async_bridge_fns(
5690                    scope,
5691                    &bridge_ctx as *const BridgeCallContext,
5692                    &pending as *const bridge::PendingPromises,
5693                    &["_asyncFn"],
5694                );
5695            }
5696
5697            // Register HTTP dispatch and create pending promise
5698            eval(
5699                &mut iso,
5700                &ctx,
5701                "var _httpEvents = []; \
5702                 globalThis._httpServerDispatch = function(eventType, payload) { \
5703                     _httpEvents.push({ type: eventType, data: payload }); \
5704                 }; \
5705                 _asyncFn('keep-alive')",
5706            );
5707            assert_eq!(pending.len(), 1);
5708
5709            let (tx, rx) = crossbeam_channel::unbounded();
5710
5711            // Send http_request event with request data
5712            let http_payload =
5713                v8_serialize_eval(&mut iso, &ctx, "({method: 'GET', url: '/api/test'})");
5714            tx.send(crate::session::SessionCommand::Message(
5715                crate::runtime_protocol::SessionMessage::StreamEvent(
5716                    crate::runtime_protocol::StreamEvent {
5717                        event_type: "http_request".into(),
5718                        payload: http_payload,
5719                    },
5720                ),
5721            ))
5722            .unwrap();
5723
5724            // Resolve the pending promise to exit the event loop
5725            let r = v8_serialize_null(&mut iso, &ctx);
5726            tx.send(crate::session::SessionCommand::Message(
5727                crate::runtime_protocol::SessionMessage::BridgeResponse(
5728                    crate::runtime_protocol::BridgeResponse {
5729                        call_id: 1,
5730                        status: 0,
5731                        payload: r,
5732                    },
5733                ),
5734            ))
5735            .unwrap();
5736
5737            let completed = {
5738                let scope = &mut v8::HandleScope::new(&mut iso);
5739                let local = v8::Local::new(scope, &ctx);
5740                let scope = &mut v8::ContextScope::new(scope, local);
5741                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5742            };
5743
5744            assert!(matches!(
5745                completed,
5746                crate::session::EventLoopStatus::Completed
5747            ));
5748            assert_eq!(eval(&mut iso, &ctx, "_httpEvents.length"), "1");
5749            assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].type"), "http_request");
5750            assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].data.method"), "GET");
5751            assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].data.url"), "/api/test");
5752        }
5753
5754        // --- Part 40: StreamEvent with unknown event_type is ignored ---
5755        {
5756            let mut iso = isolate::create_isolate(None);
5757            let ctx = isolate::create_context(&mut iso);
5758
5759            let bridge_ctx = BridgeCallContext::new(
5760                Box::new(Vec::new()),
5761                Box::new(Cursor::new(Vec::new())),
5762                "test-session".into(),
5763            );
5764            let pending = bridge::PendingPromises::new();
5765
5766            let _fn_store;
5767            {
5768                let scope = &mut v8::HandleScope::new(&mut iso);
5769                let local = v8::Local::new(scope, &ctx);
5770                let scope = &mut v8::ContextScope::new(scope, local);
5771                _fn_store = bridge::register_async_bridge_fns(
5772                    scope,
5773                    &bridge_ctx as *const BridgeCallContext,
5774                    &pending as *const bridge::PendingPromises,
5775                    &["_asyncFn"],
5776                );
5777            }
5778
5779            eval(
5780                &mut iso,
5781                &ctx,
5782                "var _anyDispatched = false; \
5783                 globalThis._childProcessDispatch = function() { _anyDispatched = true; }; \
5784                 globalThis._httpServerDispatch = function() { _anyDispatched = true; }; \
5785                 _asyncFn('keep-alive')",
5786            );
5787            assert_eq!(pending.len(), 1);
5788
5789            let (tx, rx) = crossbeam_channel::unbounded();
5790
5791            // Send unknown event type
5792            let payload = v8_serialize_null(&mut iso, &ctx);
5793            tx.send(crate::session::SessionCommand::Message(
5794                crate::runtime_protocol::SessionMessage::StreamEvent(
5795                    crate::runtime_protocol::StreamEvent {
5796                        event_type: "unknown_event".into(),
5797                        payload,
5798                    },
5799                ),
5800            ))
5801            .unwrap();
5802
5803            // Resolve pending promise to exit loop
5804            let r = v8_serialize_null(&mut iso, &ctx);
5805            tx.send(crate::session::SessionCommand::Message(
5806                crate::runtime_protocol::SessionMessage::BridgeResponse(
5807                    crate::runtime_protocol::BridgeResponse {
5808                        call_id: 1,
5809                        status: 0,
5810                        payload: r,
5811                    },
5812                ),
5813            ))
5814            .unwrap();
5815
5816            let completed = {
5817                let scope = &mut v8::HandleScope::new(&mut iso);
5818                let local = v8::Local::new(scope, &ctx);
5819                let scope = &mut v8::ContextScope::new(scope, local);
5820                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5821            };
5822
5823            assert!(matches!(
5824                completed,
5825                crate::session::EventLoopStatus::Completed
5826            ));
5827            // Unknown event should NOT have dispatched to any handler
5828            assert!(eval_bool(&mut iso, &ctx, "_anyDispatched === false"));
5829        }
5830
5831        // --- Part 41: StreamEvent dispatch with missing callback is safe (no crash) ---
5832        {
5833            let mut iso = isolate::create_isolate(None);
5834            let ctx = isolate::create_context(&mut iso);
5835
5836            let bridge_ctx = BridgeCallContext::new(
5837                Box::new(Vec::new()),
5838                Box::new(Cursor::new(Vec::new())),
5839                "test-session".into(),
5840            );
5841            let pending = bridge::PendingPromises::new();
5842
5843            let _fn_store;
5844            {
5845                let scope = &mut v8::HandleScope::new(&mut iso);
5846                let local = v8::Local::new(scope, &ctx);
5847                let scope = &mut v8::ContextScope::new(scope, local);
5848                _fn_store = bridge::register_async_bridge_fns(
5849                    scope,
5850                    &bridge_ctx as *const BridgeCallContext,
5851                    &pending as *const bridge::PendingPromises,
5852                    &["_asyncFn"],
5853                );
5854            }
5855
5856            // No dispatch functions registered, just create a pending promise
5857            eval(&mut iso, &ctx, "_asyncFn('keep-alive')");
5858            assert_eq!(pending.len(), 1);
5859
5860            let (tx, rx) = crossbeam_channel::unbounded();
5861
5862            // Send child_stdout without _childProcessDispatch registered
5863            let payload = v8_serialize_str(&mut iso, &ctx, "data");
5864            tx.send(crate::session::SessionCommand::Message(
5865                crate::runtime_protocol::SessionMessage::StreamEvent(
5866                    crate::runtime_protocol::StreamEvent {
5867                        event_type: "child_stdout".into(),
5868                        payload,
5869                    },
5870                ),
5871            ))
5872            .unwrap();
5873
5874            // Resolve pending promise
5875            let r = v8_serialize_null(&mut iso, &ctx);
5876            tx.send(crate::session::SessionCommand::Message(
5877                crate::runtime_protocol::SessionMessage::BridgeResponse(
5878                    crate::runtime_protocol::BridgeResponse {
5879                        call_id: 1,
5880                        status: 0,
5881                        payload: r,
5882                    },
5883                ),
5884            ))
5885            .unwrap();
5886
5887            // Should not crash even without dispatch function registered
5888            let completed = {
5889                let scope = &mut v8::HandleScope::new(&mut iso);
5890                let local = v8::Local::new(scope, &ctx);
5891                let scope = &mut v8::ContextScope::new(scope, local);
5892                crate::session::run_event_loop(scope, &rx, &pending, None, None)
5893            };
5894
5895            assert!(matches!(
5896                completed,
5897                crate::session::EventLoopStatus::Completed
5898            ));
5899        }
5900
5901        // --- Part 42: StreamEvent microtasks flushed after dispatch ---
5902        {
5903            let mut iso = isolate::create_isolate(None);
5904            let ctx = isolate::create_context(&mut iso);
5905
5906            let bridge_ctx = BridgeCallContext::new(
5907                Box::new(Vec::new()),
5908                Box::new(Cursor::new(Vec::new())),
5909                "test-session".into(),
5910            );
5911            let pending = bridge::PendingPromises::new();
5912
5913            let _fn_store;
5914            {
5915                let scope = &mut v8::HandleScope::new(&mut iso);
5916                let local = v8::Local::new(scope, &ctx);
5917                let scope = &mut v8::ContextScope::new(scope, local);
5918                _fn_store = bridge::register_async_bridge_fns(
5919                    scope,
5920                    &bridge_ctx as *const BridgeCallContext,
5921                    &pending as *const bridge::PendingPromises,
5922                    &["_asyncFn"],
5923                );
5924            }
5925
5926            // Set up dispatch that enqueues a microtask via Promise.resolve().then()
5927            eval(
5928                &mut iso,
5929                &ctx,
5930                "var _microtaskRanFromStream = false; \
5931                 globalThis._childProcessDispatch = function(eventType, payload) { \
5932                     Promise.resolve().then(function() { _microtaskRanFromStream = true; }); \
5933                 }; \
5934                 _asyncFn('keep-alive')",
5935            );
5936            assert_eq!(pending.len(), 1);
5937
5938            let (tx, rx) = crossbeam_channel::unbounded();
5939
5940            let payload = v8_serialize_str(&mut iso, &ctx, "data");
5941            tx.send(crate::session::SessionCommand::Message(
5942                crate::runtime_protocol::SessionMessage::StreamEvent(
5943                    crate::runtime_protocol::StreamEvent {
5944                        event_type: "child_stdout".into(),
5945                        payload,
5946                    },
5947                ),
5948            ))
5949            .unwrap();
5950
5951            // Resolve pending promise
5952            let r = v8_serialize_null(&mut iso, &ctx);
5953            tx.send(crate::session::SessionCommand::Message(
5954                crate::runtime_protocol::SessionMessage::BridgeResponse(
5955                    crate::runtime_protocol::BridgeResponse {
5956                        call_id: 1,
5957                        status: 0,
5958                        payload: r,
5959                    },
5960                ),
5961            ))
5962            .unwrap();
5963
5964            {
5965                let scope = &mut v8::HandleScope::new(&mut iso);
5966                let local = v8::Local::new(scope, &ctx);
5967                let scope = &mut v8::ContextScope::new(scope, local);
5968                crate::session::run_event_loop(scope, &rx, &pending, None, None);
5969            }
5970
5971            // Microtask enqueued by the dispatch callback should have run
5972            assert!(eval_bool(
5973                &mut iso,
5974                &ctx,
5975                "_microtaskRanFromStream === true"
5976            ));
5977        }
5978
5979        // --- Part 43: Timeout terminates infinite loop ---
5980        {
5981            let mut iso = isolate::create_isolate(None);
5982            let ctx = isolate::create_context(&mut iso);
5983
5984            // Create abort channel for timeout
5985            let (abort_tx, _abort_rx) = crossbeam_channel::bounded::<()>(0);
5986
5987            // Get isolate handle for the timeout guard
5988            let iso_handle = iso.thread_safe_handle();
5989
5990            // Start a 50ms timeout
5991            let mut guard = crate::timeout::TimeoutGuard::new(50, iso_handle, abort_tx)
5992                .expect("timeout guard should start");
5993
5994            // Run an infinite loop — timeout should terminate it
5995            let (code, error) = {
5996                let scope = &mut v8::HandleScope::new(&mut iso);
5997                let local = v8::Local::new(scope, &ctx);
5998                let scope = &mut v8::ContextScope::new(scope, local);
5999                execute_script(scope, "", "while(true) {}", &mut None)
6000            };
6001
6002            assert!(guard.timed_out(), "timeout should have fired");
6003            // V8 termination causes an error
6004            assert_eq!(code, 1);
6005            assert!(error.is_some());
6006
6007            guard.cancel();
6008        }
6009
6010        // --- Part 44: Timeout cancelled when execution completes before deadline ---
6011        {
6012            let mut iso = isolate::create_isolate(None);
6013            let ctx = isolate::create_context(&mut iso);
6014
6015            let (abort_tx, _abort_rx) = crossbeam_channel::bounded::<()>(0);
6016            let iso_handle = iso.thread_safe_handle();
6017
6018            // 5 second timeout — execution completes well before
6019            let mut guard = crate::timeout::TimeoutGuard::new(5000, iso_handle, abort_tx)
6020                .expect("timeout guard should start");
6021
6022            let (code, error) = {
6023                let scope = &mut v8::HandleScope::new(&mut iso);
6024                let local = v8::Local::new(scope, &ctx);
6025                let scope = &mut v8::ContextScope::new(scope, local);
6026                execute_script(scope, "", "1 + 1", &mut None)
6027            };
6028
6029            assert!(!guard.timed_out(), "timeout should not have fired");
6030            assert_eq!(code, 0);
6031            assert!(error.is_none());
6032
6033            guard.cancel();
6034        }
6035
6036        // --- Part 45: Timeout fires during sync bridge call (unblocks channel reader) ---
6037        {
6038            let mut iso = isolate::create_isolate(None);
6039            let ctx = isolate::create_context(&mut iso);
6040
6041            // Set up abort channel for timeout
6042            let (abort_tx, abort_rx) = crossbeam_channel::bounded::<()>(0);
6043            let iso_handle = iso.thread_safe_handle();
6044
6045            // Create a BridgeCallContext with a channel reader that monitors abort_rx
6046            // Simulate: JS calls a sync bridge function, but no response comes back.
6047            // The timeout should unblock the reader via abort channel.
6048            let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
6049
6050            // Writer goes to a buffer (we don't care about outgoing messages)
6051            let writer_buf = Arc::new(Mutex::new(Vec::new()));
6052
6053            // Create the bridge context with a channel-based reader
6054            // We can't use ChannelMessageReader directly (it's #[cfg(not(test))])
6055            // Instead, test the abort_rx behavior through run_event_loop
6056
6057            let pending = bridge::PendingPromises::new();
6058
6059            // Register an async bridge function that sends a BridgeCall
6060            let bridge_ctx = BridgeCallContext::new(
6061                Box::new(SharedWriter(Arc::clone(&writer_buf))),
6062                Box::new(Cursor::new(Vec::new())), // unused for async
6063                "test-session".into(),
6064            );
6065            let _async_store;
6066            {
6067                let scope = &mut v8::HandleScope::new(&mut iso);
6068                let local = v8::Local::new(scope, &ctx);
6069                let scope = &mut v8::ContextScope::new(scope, local);
6070                _async_store = bridge::register_async_bridge_fns(
6071                    scope,
6072                    &bridge_ctx as *const BridgeCallContext,
6073                    &pending as *const bridge::PendingPromises,
6074                    &["_slowFn"],
6075                );
6076            }
6077
6078            // Execute code that calls async bridge function (creates a pending promise)
6079            let (_code, _error) = {
6080                let scope = &mut v8::HandleScope::new(&mut iso);
6081                let local = v8::Local::new(scope, &ctx);
6082                let scope = &mut v8::ContextScope::new(scope, local);
6083                execute_script(scope, "", "_slowFn('never-responds')", &mut None)
6084            };
6085
6086            assert_eq!(pending.len(), 1, "should have 1 pending promise");
6087
6088            // Start a 50ms timeout
6089            let mut guard = crate::timeout::TimeoutGuard::new(50, iso_handle, abort_tx)
6090                .expect("timeout guard should start");
6091
6092            // Run event loop — it should be terminated by the timeout
6093            // (no messages on cmd_rx, so it blocks until abort_rx fires)
6094            let completed = {
6095                let scope = &mut v8::HandleScope::new(&mut iso);
6096                let local = v8::Local::new(scope, &ctx);
6097                let scope = &mut v8::ContextScope::new(scope, local);
6098                crate::session::run_event_loop(scope, &cmd_rx, &pending, Some(&abort_rx), None)
6099            };
6100
6101            assert!(
6102                matches!(completed, crate::session::EventLoopStatus::Terminated),
6103                "event loop should have been terminated"
6104            );
6105            assert!(guard.timed_out(), "timeout should have fired");
6106
6107            guard.cancel();
6108            drop(cmd_tx); // clean up
6109        }
6110
6111        // --- Part 46: Timeout error message structure ---
6112        {
6113            // Verify that the timeout error produced by the session matches expectations.
6114            // This tests the ipc::ExecutionError structure, not V8 directly.
6115            let err = crate::ipc::ExecutionError {
6116                error_type: "Error".into(),
6117                message: "Script execution timed out".into(),
6118                stack: String::new(),
6119                code: Some("ERR_SCRIPT_EXECUTION_TIMEOUT".into()),
6120            };
6121            assert_eq!(err.error_type, "Error");
6122            assert_eq!(err.message, "Script execution timed out");
6123            assert_eq!(err.code, Some("ERR_SCRIPT_EXECUTION_TIMEOUT".into()));
6124        }
6125
6126        // --- Part 47: ProcessExitError detected via _isProcessExit sentinel ---
6127        {
6128            let mut iso = isolate::create_isolate(None);
6129            let ctx = isolate::create_context(&mut iso);
6130
6131            let scope = &mut v8::HandleScope::new(&mut iso);
6132            let local = v8::Local::new(scope, &ctx);
6133            let scope = &mut v8::ContextScope::new(scope, local);
6134
6135            // Simulate ProcessExitError: an Error object with _isProcessExit: true and code: 42
6136            let code = r#"
6137                var err = new Error("process.exit(42)");
6138                err._isProcessExit = true;
6139                err.code = 42;
6140                throw err;
6141            "#;
6142
6143            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6144            assert_eq!(
6145                exit_code, 42,
6146                "ProcessExitError should return the error's exit code"
6147            );
6148            let err = error.unwrap();
6149            assert_eq!(err.error_type, "Error");
6150            assert!(err.message.contains("process.exit(42)"));
6151            // Numeric .code should NOT appear in the string code field
6152            assert_eq!(err.code, None);
6153        }
6154
6155        // --- Part 48: ProcessExitError with exit code 0 ---
6156        {
6157            let mut iso = isolate::create_isolate(None);
6158            let ctx = isolate::create_context(&mut iso);
6159
6160            let scope = &mut v8::HandleScope::new(&mut iso);
6161            let local = v8::Local::new(scope, &ctx);
6162            let scope = &mut v8::ContextScope::new(scope, local);
6163
6164            let code = r#"
6165                var err = new Error("process.exit(0)");
6166                err._isProcessExit = true;
6167                err.code = 0;
6168                throw err;
6169            "#;
6170
6171            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6172            assert_eq!(
6173                exit_code, 0,
6174                "ProcessExitError code 0 should return exit code 0"
6175            );
6176            assert!(error.is_some());
6177        }
6178
6179        // --- Part 49: Non-ProcessExitError returns exit code 1 ---
6180        {
6181            let mut iso = isolate::create_isolate(None);
6182            let ctx = isolate::create_context(&mut iso);
6183
6184            let scope = &mut v8::HandleScope::new(&mut iso);
6185            let local = v8::Local::new(scope, &ctx);
6186            let scope = &mut v8::ContextScope::new(scope, local);
6187
6188            // Regular error without _isProcessExit sentinel
6189            let code = r#"throw new TypeError("not a process exit")"#;
6190
6191            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6192            assert_eq!(exit_code, 1, "Regular errors should return exit code 1");
6193            let err = error.unwrap();
6194            assert_eq!(err.error_type, "TypeError");
6195            assert_eq!(err.message, "not a process exit");
6196        }
6197
6198        // --- Part 50: ProcessExitError with custom constructor name ---
6199        {
6200            let mut iso = isolate::create_isolate(None);
6201            let ctx = isolate::create_context(&mut iso);
6202
6203            let scope = &mut v8::HandleScope::new(&mut iso);
6204            let local = v8::Local::new(scope, &ctx);
6205            let scope = &mut v8::ContextScope::new(scope, local);
6206
6207            // Custom ProcessExitError class
6208            let code = r#"
6209                class ProcessExitError extends Error {
6210                    constructor(exitCode) {
6211                        super("process exited with code " + exitCode);
6212                        this._isProcessExit = true;
6213                        this.code = exitCode;
6214                    }
6215                }
6216                throw new ProcessExitError(7);
6217            "#;
6218
6219            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6220            assert_eq!(exit_code, 7);
6221            let err = error.unwrap();
6222            assert_eq!(err.error_type, "ProcessExitError");
6223            assert!(err.message.contains("process exited with code 7"));
6224        }
6225
6226        // --- Part 51: extract_process_exit_code returns None for non-objects ---
6227        {
6228            let mut iso = isolate::create_isolate(None);
6229            let ctx = isolate::create_context(&mut iso);
6230
6231            let scope = &mut v8::HandleScope::new(&mut iso);
6232            let local = v8::Local::new(scope, &ctx);
6233            let scope = &mut v8::ContextScope::new(scope, local);
6234
6235            // Thrown string — not an object, should not be detected as ProcessExitError
6236            let code = r#"throw "just a string""#;
6237            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6238            assert_eq!(exit_code, 1);
6239            let err = error.unwrap();
6240            assert_eq!(err.error_type, "Error");
6241            assert_eq!(err.message, "just a string");
6242
6243            // Object without _isProcessExit sentinel
6244            let code2 = r#"
6245                var obj = new Error("no sentinel");
6246                obj._isProcessExit = false;
6247                obj.code = 99;
6248                throw obj;
6249            "#;
6250            let (exit_code2, error2) = execute_script(scope, "", code2, &mut None);
6251            assert_eq!(exit_code2, 1, "_isProcessExit:false should not be detected");
6252            assert!(error2.is_some());
6253        }
6254
6255        // --- Part 52: Error with string code field (Node-style) preserved ---
6256        {
6257            let mut iso = isolate::create_isolate(None);
6258            let ctx = isolate::create_context(&mut iso);
6259
6260            let scope = &mut v8::HandleScope::new(&mut iso);
6261            let local = v8::Local::new(scope, &ctx);
6262            let scope = &mut v8::ContextScope::new(scope, local);
6263
6264            let code = r#"
6265                var err = new Error("Cannot find module './missing'");
6266                err.code = "ERR_MODULE_NOT_FOUND";
6267                throw err;
6268            "#;
6269
6270            let (exit_code, error) = execute_script(scope, "", code, &mut None);
6271            assert_eq!(exit_code, 1);
6272            let err = error.unwrap();
6273            assert_eq!(err.error_type, "Error");
6274            assert_eq!(err.code, Some("ERR_MODULE_NOT_FOUND".into()));
6275        }
6276
6277        // --- Part 53: Error type from constructor name for standard errors ---
6278        {
6279            let mut iso = isolate::create_isolate(None);
6280            let ctx = isolate::create_context(&mut iso);
6281
6282            let scope = &mut v8::HandleScope::new(&mut iso);
6283            let local = v8::Local::new(scope, &ctx);
6284            let scope = &mut v8::ContextScope::new(scope, local);
6285
6286            // SyntaxError
6287            let (_, err) = execute_script(scope, "", "eval('function(')", &mut None);
6288            let err = err.unwrap();
6289            assert_eq!(err.error_type, "SyntaxError");
6290
6291            // RangeError
6292            let (_, err2) = execute_script(scope, "", "new Array(-1)", &mut None);
6293            let err2 = err2.unwrap();
6294            assert_eq!(err2.error_type, "RangeError");
6295
6296            // ReferenceError
6297            let (_, err3) = execute_script(scope, "", "undefinedVariable", &mut None);
6298            let err3 = err3.unwrap();
6299            assert_eq!(err3.error_type, "ReferenceError");
6300        }
6301
6302        // --- Part 54: process.exitCode is honored for synchronous completion ---
6303        {
6304            let mut iso = isolate::create_isolate(None);
6305            let ctx = isolate::create_context(&mut iso);
6306
6307            let scope = &mut v8::HandleScope::new(&mut iso);
6308            let local = v8::Local::new(scope, &ctx);
6309            let scope = &mut v8::ContextScope::new(scope, local);
6310            execute_script(
6311                scope,
6312                "",
6313                "globalThis.process = { exitCode: 0 };",
6314                &mut None,
6315            );
6316
6317            let (exit_code, error) = execute_script(scope, "", "process.exitCode = 3;", &mut None);
6318            assert_eq!(exit_code, 3);
6319            assert!(error.is_none());
6320        }
6321
6322        // --- Part 55: process.exitCode is honored for fulfilled async completion ---
6323        {
6324            let mut iso = isolate::create_isolate(None);
6325            let ctx = isolate::create_context(&mut iso);
6326
6327            let scope = &mut v8::HandleScope::new(&mut iso);
6328            let local = v8::Local::new(scope, &ctx);
6329            let scope = &mut v8::ContextScope::new(scope, local);
6330            execute_script(
6331                scope,
6332                "",
6333                "globalThis.process = { exitCode: 0 };",
6334                &mut None,
6335            );
6336
6337            let (exit_code, error) = execute_script(
6338                scope,
6339                "",
6340                "(async () => { process.exitCode = 4; })()",
6341                &mut None,
6342            );
6343            assert_eq!(exit_code, 4);
6344            assert!(error.is_none());
6345        }
6346
6347        // --- Part 54: Stack trace extracted from error.stack property ---
6348        {
6349            let mut iso = isolate::create_isolate(None);
6350            let ctx = isolate::create_context(&mut iso);
6351
6352            let scope = &mut v8::HandleScope::new(&mut iso);
6353            let local = v8::Local::new(scope, &ctx);
6354            let scope = &mut v8::ContextScope::new(scope, local);
6355
6356            let code = r#"
6357                function innerFn() { throw new Error("deep error"); }
6358                function outerFn() { innerFn(); }
6359                outerFn();
6360            "#;
6361
6362            let (_, error) = execute_script(scope, "", code, &mut None);
6363            let err = error.unwrap();
6364            assert_eq!(err.error_type, "Error");
6365            assert_eq!(err.message, "deep error");
6366            assert!(
6367                err.stack.contains("innerFn"),
6368                "stack should contain innerFn"
6369            );
6370            assert!(
6371                err.stack.contains("outerFn"),
6372                "stack should contain outerFn"
6373            );
6374        }
6375
6376        // --- V8 ValueSerializer/ValueDeserializer round-trip tests ---
6377
6378        // Part 55: Primitives round-trip (null, undefined, true, false, integers, floats)
6379        {
6380            use crate::bridge::{
6381                deserialize_v8_wire_value as deserialize_v8_value,
6382                serialize_v8_wire_value as serialize_v8_value,
6383            };
6384
6385            let mut iso = isolate::create_isolate(None);
6386            let ctx = isolate::create_context(&mut iso);
6387            let scope = &mut v8::HandleScope::new(&mut iso);
6388            let local = v8::Local::new(scope, &ctx);
6389            let scope = &mut v8::ContextScope::new(scope, local);
6390
6391            // null
6392            let null_val = v8::null(scope).into();
6393            let bytes = serialize_v8_value(scope, null_val).unwrap();
6394            let out = deserialize_v8_value(scope, &bytes).unwrap();
6395            assert!(out.is_null());
6396
6397            // undefined
6398            let undef_val = v8::undefined(scope).into();
6399            let bytes = serialize_v8_value(scope, undef_val).unwrap();
6400            let out = deserialize_v8_value(scope, &bytes).unwrap();
6401            assert!(out.is_undefined());
6402
6403            // true
6404            let bool_val = v8::Boolean::new(scope, true).into();
6405            let bytes = serialize_v8_value(scope, bool_val).unwrap();
6406            let out = deserialize_v8_value(scope, &bytes).unwrap();
6407            assert!(out.is_true());
6408
6409            // false
6410            let bool_val = v8::Boolean::new(scope, false).into();
6411            let bytes = serialize_v8_value(scope, bool_val).unwrap();
6412            let out = deserialize_v8_value(scope, &bytes).unwrap();
6413            assert!(out.is_false());
6414
6415            // integer
6416            let num_val: v8::Local<v8::Value> = v8::Integer::new(scope, 42).into();
6417            let bytes = serialize_v8_value(scope, num_val).unwrap();
6418            let out = deserialize_v8_value(scope, &bytes).unwrap();
6419            assert_eq!(out.int32_value(scope).unwrap(), 42);
6420
6421            // negative integer
6422            let num_val: v8::Local<v8::Value> = v8::Integer::new(scope, -7).into();
6423            let bytes = serialize_v8_value(scope, num_val).unwrap();
6424            let out = deserialize_v8_value(scope, &bytes).unwrap();
6425            assert_eq!(out.int32_value(scope).unwrap(), -7);
6426
6427            // float
6428            let num_val: v8::Local<v8::Value> = v8::Number::new(scope, 3.125).into();
6429            let bytes = serialize_v8_value(scope, num_val).unwrap();
6430            let out = deserialize_v8_value(scope, &bytes).unwrap();
6431            assert!((out.number_value(scope).unwrap() - 3.125).abs() < 1e-10);
6432        }
6433
6434        // Part 56: Strings round-trip
6435        {
6436            use crate::bridge::{
6437                deserialize_v8_wire_value as deserialize_v8_value,
6438                serialize_v8_wire_value as serialize_v8_value,
6439            };
6440
6441            let mut iso = isolate::create_isolate(None);
6442            let ctx = isolate::create_context(&mut iso);
6443            let scope = &mut v8::HandleScope::new(&mut iso);
6444            let local = v8::Local::new(scope, &ctx);
6445            let scope = &mut v8::ContextScope::new(scope, local);
6446
6447            // ASCII string
6448            let s = v8::String::new(scope, "hello world").unwrap();
6449            let bytes = serialize_v8_value(scope, s.into()).unwrap();
6450            let out = deserialize_v8_value(scope, &bytes).unwrap();
6451            assert!(out.is_string());
6452            assert_eq!(out.to_rust_string_lossy(scope), "hello world");
6453
6454            // Empty string
6455            let s = v8::String::new(scope, "").unwrap();
6456            let bytes = serialize_v8_value(scope, s.into()).unwrap();
6457            let out = deserialize_v8_value(scope, &bytes).unwrap();
6458            assert!(out.is_string());
6459            assert_eq!(out.to_rust_string_lossy(scope), "");
6460
6461            // Unicode string
6462            let s = v8::String::new(scope, "hello 🌍 world").unwrap();
6463            let bytes = serialize_v8_value(scope, s.into()).unwrap();
6464            let out = deserialize_v8_value(scope, &bytes).unwrap();
6465            assert_eq!(out.to_rust_string_lossy(scope), "hello 🌍 world");
6466        }
6467
6468        // Part 57: Arrays round-trip
6469        {
6470            use crate::bridge::{
6471                deserialize_v8_wire_value as deserialize_v8_value,
6472                serialize_v8_wire_value as serialize_v8_value,
6473            };
6474
6475            let mut iso = isolate::create_isolate(None);
6476            let ctx = isolate::create_context(&mut iso);
6477            let scope = &mut v8::HandleScope::new(&mut iso);
6478            let local = v8::Local::new(scope, &ctx);
6479            let scope = &mut v8::ContextScope::new(scope, local);
6480
6481            // [1, "two", true, null]
6482            let arr = v8::Array::new(scope, 4);
6483            let v1: v8::Local<v8::Value> = v8::Integer::new(scope, 1).into();
6484            let v2: v8::Local<v8::Value> = v8::String::new(scope, "two").unwrap().into();
6485            let v3: v8::Local<v8::Value> = v8::Boolean::new(scope, true).into();
6486            let v4: v8::Local<v8::Value> = v8::null(scope).into();
6487            arr.set_index(scope, 0, v1);
6488            arr.set_index(scope, 1, v2);
6489            arr.set_index(scope, 2, v3);
6490            arr.set_index(scope, 3, v4);
6491
6492            let bytes = serialize_v8_value(scope, arr.into()).unwrap();
6493            let out = deserialize_v8_value(scope, &bytes).unwrap();
6494            assert!(out.is_array());
6495            let out_arr = v8::Local::<v8::Array>::try_from(out).unwrap();
6496            assert_eq!(out_arr.length(), 4);
6497            assert_eq!(
6498                out_arr
6499                    .get_index(scope, 0)
6500                    .unwrap()
6501                    .int32_value(scope)
6502                    .unwrap(),
6503                1
6504            );
6505            assert_eq!(
6506                out_arr
6507                    .get_index(scope, 1)
6508                    .unwrap()
6509                    .to_rust_string_lossy(scope),
6510                "two"
6511            );
6512            assert!(out_arr.get_index(scope, 2).unwrap().is_true());
6513            assert!(out_arr.get_index(scope, 3).unwrap().is_null());
6514
6515            // Empty array
6516            let empty_arr = v8::Array::new(scope, 0);
6517            let bytes = serialize_v8_value(scope, empty_arr.into()).unwrap();
6518            let out = deserialize_v8_value(scope, &bytes).unwrap();
6519            assert!(out.is_array());
6520            assert_eq!(v8::Local::<v8::Array>::try_from(out).unwrap().length(), 0);
6521        }
6522
6523        // Part 58: Objects round-trip
6524        {
6525            use crate::bridge::{
6526                deserialize_v8_wire_value as deserialize_v8_value,
6527                serialize_v8_wire_value as serialize_v8_value,
6528            };
6529
6530            let mut iso = isolate::create_isolate(None);
6531            let ctx = isolate::create_context(&mut iso);
6532            let scope = &mut v8::HandleScope::new(&mut iso);
6533            let local = v8::Local::new(scope, &ctx);
6534            let scope = &mut v8::ContextScope::new(scope, local);
6535
6536            // { name: "test", count: 42, active: true }
6537            let obj = v8::Object::new(scope);
6538            let k1 = v8::String::new(scope, "name").unwrap();
6539            let v1: v8::Local<v8::Value> = v8::String::new(scope, "test").unwrap().into();
6540            let k2 = v8::String::new(scope, "count").unwrap();
6541            let v2: v8::Local<v8::Value> = v8::Integer::new(scope, 42).into();
6542            let k3 = v8::String::new(scope, "active").unwrap();
6543            let v3: v8::Local<v8::Value> = v8::Boolean::new(scope, true).into();
6544            obj.set(scope, k1.into(), v1);
6545            obj.set(scope, k2.into(), v2);
6546            obj.set(scope, k3.into(), v3);
6547
6548            let bytes = serialize_v8_value(scope, obj.into()).unwrap();
6549            let out = deserialize_v8_value(scope, &bytes).unwrap();
6550            assert!(out.is_object());
6551            let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6552            let k = v8::String::new(scope, "name").unwrap();
6553            assert_eq!(
6554                out_obj
6555                    .get(scope, k.into())
6556                    .unwrap()
6557                    .to_rust_string_lossy(scope),
6558                "test"
6559            );
6560            let k = v8::String::new(scope, "count").unwrap();
6561            assert_eq!(
6562                out_obj
6563                    .get(scope, k.into())
6564                    .unwrap()
6565                    .int32_value(scope)
6566                    .unwrap(),
6567                42
6568            );
6569            let k = v8::String::new(scope, "active").unwrap();
6570            assert!(out_obj.get(scope, k.into()).unwrap().is_true());
6571        }
6572
6573        // Part 59: Uint8Array round-trip
6574        {
6575            use crate::bridge::{
6576                deserialize_v8_wire_value as deserialize_v8_value,
6577                serialize_v8_wire_value as serialize_v8_value,
6578            };
6579
6580            let mut iso = isolate::create_isolate(None);
6581            let ctx = isolate::create_context(&mut iso);
6582            let scope = &mut v8::HandleScope::new(&mut iso);
6583            let local = v8::Local::new(scope, &ctx);
6584            let scope = &mut v8::ContextScope::new(scope, local);
6585
6586            let data = [0u8, 1, 2, 255, 128, 64];
6587            let ab = v8::ArrayBuffer::new(scope, data.len());
6588            {
6589                let bs = ab.get_backing_store();
6590                unsafe {
6591                    std::ptr::copy_nonoverlapping(
6592                        data.as_ptr(),
6593                        bs.data().unwrap().as_ptr() as *mut u8,
6594                        data.len(),
6595                    );
6596                }
6597            }
6598            let u8arr = v8::Uint8Array::new(scope, ab, 0, data.len()).unwrap();
6599
6600            let bytes = serialize_v8_value(scope, u8arr.into()).unwrap();
6601            let out = deserialize_v8_value(scope, &bytes).unwrap();
6602            assert!(out.is_uint8_array());
6603            let out_arr = v8::Local::<v8::Uint8Array>::try_from(out).unwrap();
6604            assert_eq!(out_arr.byte_length(), 6);
6605            let mut buf = vec![0u8; 6];
6606            out_arr.copy_contents(&mut buf);
6607            assert_eq!(buf, vec![0, 1, 2, 255, 128, 64]);
6608        }
6609
6610        // Part 60: Nested structures round-trip
6611        {
6612            use crate::bridge::{
6613                deserialize_v8_wire_value as deserialize_v8_value,
6614                serialize_v8_wire_value as serialize_v8_value,
6615            };
6616
6617            let mut iso = isolate::create_isolate(None);
6618            let ctx = isolate::create_context(&mut iso);
6619            let scope = &mut v8::HandleScope::new(&mut iso);
6620            let local = v8::Local::new(scope, &ctx);
6621            let scope = &mut v8::ContextScope::new(scope, local);
6622
6623            // Build via JS: { items: [1, { nested: "value" }], flag: false }
6624            let code = r#"
6625                ({
6626                    items: [1, { nested: "value" }],
6627                    flag: false
6628                })
6629            "#;
6630            let source = v8::String::new(scope, code).unwrap();
6631            let script = v8::Script::compile(scope, source, None).unwrap();
6632            let val = script.run(scope).unwrap();
6633
6634            let bytes = serialize_v8_value(scope, val).unwrap();
6635            let out = deserialize_v8_value(scope, &bytes).unwrap();
6636            assert!(out.is_object());
6637            let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6638
6639            // Check items array
6640            let k = v8::String::new(scope, "items").unwrap();
6641            let items = out_obj.get(scope, k.into()).unwrap();
6642            assert!(items.is_array());
6643            let items_arr = v8::Local::<v8::Array>::try_from(items).unwrap();
6644            assert_eq!(items_arr.length(), 2);
6645            assert_eq!(
6646                items_arr
6647                    .get_index(scope, 0)
6648                    .unwrap()
6649                    .int32_value(scope)
6650                    .unwrap(),
6651                1
6652            );
6653            let inner = items_arr.get_index(scope, 1).unwrap();
6654            assert!(inner.is_object());
6655            let inner_obj = v8::Local::<v8::Object>::try_from(inner).unwrap();
6656            let k = v8::String::new(scope, "nested").unwrap();
6657            assert_eq!(
6658                inner_obj
6659                    .get(scope, k.into())
6660                    .unwrap()
6661                    .to_rust_string_lossy(scope),
6662                "value"
6663            );
6664
6665            // Check flag
6666            let k = v8::String::new(scope, "flag").unwrap();
6667            assert!(out_obj.get(scope, k.into()).unwrap().is_false());
6668        }
6669
6670        // Part 61: Date, RegExp, Map, Set, Error round-trip via JS eval
6671        {
6672            use crate::bridge::{
6673                deserialize_v8_wire_value as deserialize_v8_value,
6674                serialize_v8_wire_value as serialize_v8_value,
6675            };
6676
6677            let mut iso = isolate::create_isolate(None);
6678            let ctx = isolate::create_context(&mut iso);
6679            let scope = &mut v8::HandleScope::new(&mut iso);
6680            let local = v8::Local::new(scope, &ctx);
6681            let scope = &mut v8::ContextScope::new(scope, local);
6682
6683            // Date
6684            let source = v8::String::new(scope, "new Date(1700000000000)").unwrap();
6685            let script = v8::Script::compile(scope, source, None).unwrap();
6686            let date_val = script.run(scope).unwrap();
6687            let bytes = serialize_v8_value(scope, date_val).unwrap();
6688            let out = deserialize_v8_value(scope, &bytes).unwrap();
6689            assert!(out.is_date());
6690            let date = v8::Local::<v8::Date>::try_from(out).unwrap();
6691            assert_eq!(date.value_of(), 1700000000000.0);
6692
6693            // RegExp
6694            let source = v8::String::new(scope, "/abc/gi").unwrap();
6695            let script = v8::Script::compile(scope, source, None).unwrap();
6696            let re_val = script.run(scope).unwrap();
6697            let bytes = serialize_v8_value(scope, re_val).unwrap();
6698            let out = deserialize_v8_value(scope, &bytes).unwrap();
6699            assert!(out.is_reg_exp());
6700
6701            // Map
6702            let source = v8::String::new(scope, "new Map([['a', 1], ['b', 2]])").unwrap();
6703            let script = v8::Script::compile(scope, source, None).unwrap();
6704            let map_val = script.run(scope).unwrap();
6705            let bytes = serialize_v8_value(scope, map_val).unwrap();
6706            let out = deserialize_v8_value(scope, &bytes).unwrap();
6707            assert!(out.is_map());
6708            let map = v8::Local::<v8::Map>::try_from(out).unwrap();
6709            assert_eq!(map.size(), 2);
6710
6711            // Set
6712            let source = v8::String::new(scope, "new Set([10, 20, 30])").unwrap();
6713            let script = v8::Script::compile(scope, source, None).unwrap();
6714            let set_val = script.run(scope).unwrap();
6715            let bytes = serialize_v8_value(scope, set_val).unwrap();
6716            let out = deserialize_v8_value(scope, &bytes).unwrap();
6717            assert!(out.is_set());
6718            let set = v8::Local::<v8::Set>::try_from(out).unwrap();
6719            assert_eq!(set.size(), 3);
6720
6721            // Error
6722            let source = v8::String::new(scope, "new TypeError('oops')").unwrap();
6723            let script = v8::Script::compile(scope, source, None).unwrap();
6724            let err_val = script.run(scope).unwrap();
6725            let bytes = serialize_v8_value(scope, err_val).unwrap();
6726            let out = deserialize_v8_value(scope, &bytes).unwrap();
6727            // Error is serialized as a plain object with message property
6728            assert!(out.is_object());
6729            let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6730            let k = v8::String::new(scope, "message").unwrap();
6731            let msg = out_obj.get(scope, k.into()).unwrap();
6732            assert_eq!(msg.to_rust_string_lossy(scope), "oops");
6733        }
6734
6735        // Part 62: Circular references round-trip
6736        {
6737            use crate::bridge::{
6738                deserialize_v8_wire_value as deserialize_v8_value,
6739                serialize_v8_wire_value as serialize_v8_value,
6740            };
6741
6742            let mut iso = isolate::create_isolate(None);
6743            let ctx = isolate::create_context(&mut iso);
6744            let scope = &mut v8::HandleScope::new(&mut iso);
6745            let local = v8::Local::new(scope, &ctx);
6746            let scope = &mut v8::ContextScope::new(scope, local);
6747
6748            // Build circular reference via JS
6749            let source = v8::String::new(scope, "var o = { a: 1 }; o.self = o; o").unwrap();
6750            let script = v8::Script::compile(scope, source, None).unwrap();
6751            let circ_val = script.run(scope).unwrap();
6752
6753            let bytes = serialize_v8_value(scope, circ_val).unwrap();
6754            let out = deserialize_v8_value(scope, &bytes).unwrap();
6755            assert!(out.is_object());
6756            let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6757
6758            // Verify the self-reference resolves
6759            let k = v8::String::new(scope, "a").unwrap();
6760            assert_eq!(
6761                out_obj
6762                    .get(scope, k.into())
6763                    .unwrap()
6764                    .int32_value(scope)
6765                    .unwrap(),
6766                1
6767            );
6768            let k = v8::String::new(scope, "self").unwrap();
6769            let self_ref = out_obj.get(scope, k.into()).unwrap();
6770            assert!(self_ref.is_object());
6771            // The self reference should point back to the same structure
6772            let self_obj = v8::Local::<v8::Object>::try_from(self_ref).unwrap();
6773            let k = v8::String::new(scope, "a").unwrap();
6774            assert_eq!(
6775                self_obj
6776                    .get(scope, k.into())
6777                    .unwrap()
6778                    .int32_value(scope)
6779                    .unwrap(),
6780                1
6781            );
6782        }
6783
6784        // --- V8 Code Caching tests ---
6785
6786        // Part 60: First execution populates the cache
6787        {
6788            let mut iso = isolate::create_isolate(None);
6789            let ctx = isolate::create_context(&mut iso);
6790            let mut cache: Option<BridgeCodeCache> = None;
6791
6792            let bridge = "(function() { globalThis._cached = 'yes'; })()";
6793            let (code, error) = {
6794                let scope = &mut v8::HandleScope::new(&mut iso);
6795                let local = v8::Local::new(scope, &ctx);
6796                let scope = &mut v8::ContextScope::new(scope, local);
6797                execute_script(scope, bridge, "var _saw = _cached;", &mut cache)
6798            };
6799
6800            assert_eq!(code, 0);
6801            assert!(error.is_none());
6802            assert_eq!(eval(&mut iso, &ctx, "_saw"), "yes");
6803            // Cache should be populated after first compile
6804            assert!(
6805                cache.is_some(),
6806                "cache should be populated after first execution"
6807            );
6808            assert!(!cache.as_ref().unwrap().cached_data.is_empty());
6809        }
6810
6811        // Part 61: Second execution uses the cache and produces correct results
6812        {
6813            let mut iso = isolate::create_isolate(None);
6814            let mut cache: Option<BridgeCodeCache> = None;
6815            let bridge = "(function() { globalThis._counter = (globalThis._counter || 0) + 1; })()";
6816
6817            // First execution — populates cache
6818            {
6819                let ctx = isolate::create_context(&mut iso);
6820                let (code, _) = {
6821                    let scope = &mut v8::HandleScope::new(&mut iso);
6822                    let local = v8::Local::new(scope, &ctx);
6823                    let scope = &mut v8::ContextScope::new(scope, local);
6824                    execute_script(scope, bridge, "", &mut cache)
6825                };
6826                assert_eq!(code, 0);
6827                assert!(cache.is_some());
6828            }
6829
6830            let cached_data_len = cache.as_ref().unwrap().cached_data.len();
6831
6832            // Second execution — consumes cache (fresh context)
6833            {
6834                let ctx = isolate::create_context(&mut iso);
6835                let (code, _) = {
6836                    let scope = &mut v8::HandleScope::new(&mut iso);
6837                    let local = v8::Local::new(scope, &ctx);
6838                    let scope = &mut v8::ContextScope::new(scope, local);
6839                    execute_script(scope, bridge, "", &mut cache)
6840                };
6841                assert_eq!(code, 0);
6842                // Cache should still be present (not invalidated)
6843                assert!(
6844                    cache.is_some(),
6845                    "cache should persist after second execution"
6846                );
6847                // Cached data should be same size (same code, same cache)
6848                assert_eq!(cache.as_ref().unwrap().cached_data.len(), cached_data_len);
6849                // Bridge code executed correctly
6850                assert_eq!(eval(&mut iso, &ctx, "String(_counter)"), "1");
6851            }
6852        }
6853
6854        // Part 62: Cache is invalidated when bridge code changes
6855        {
6856            let mut iso = isolate::create_isolate(None);
6857            let mut cache: Option<BridgeCodeCache> = None;
6858
6859            // Populate cache with bridge A
6860            {
6861                let ctx = isolate::create_context(&mut iso);
6862                let (code, _) = {
6863                    let scope = &mut v8::HandleScope::new(&mut iso);
6864                    let local = v8::Local::new(scope, &ctx);
6865                    let scope = &mut v8::ContextScope::new(scope, local);
6866                    execute_script(
6867                        scope,
6868                        "(function() { globalThis.x = 'A'; })()",
6869                        "",
6870                        &mut cache,
6871                    )
6872                };
6873                assert_eq!(code, 0);
6874                assert!(cache.is_some());
6875            }
6876
6877            let hash_a = cache.as_ref().unwrap().source_hash;
6878
6879            // Execute with different bridge code — cache should be replaced
6880            {
6881                let ctx = isolate::create_context(&mut iso);
6882                let (code, _) = {
6883                    let scope = &mut v8::HandleScope::new(&mut iso);
6884                    let local = v8::Local::new(scope, &ctx);
6885                    let scope = &mut v8::ContextScope::new(scope, local);
6886                    execute_script(
6887                        scope,
6888                        "(function() { globalThis.x = 'B'; })()",
6889                        "",
6890                        &mut cache,
6891                    )
6892                };
6893                assert_eq!(code, 0);
6894                assert!(cache.is_some());
6895                // Hash should be different
6896                assert_ne!(cache.as_ref().unwrap().source_hash, hash_a);
6897                // Code should have executed correctly
6898                assert_eq!(eval(&mut iso, &ctx, "x"), "B");
6899            }
6900        }
6901
6902        // Part 63: Code caching works with execute_module
6903        {
6904            let mut iso = isolate::create_isolate(None);
6905            let mut cache: Option<BridgeCodeCache> = None;
6906
6907            let output = Arc::new(Mutex::new(Vec::new()));
6908            let writer = SharedWriter(Arc::clone(&output));
6909            let reader = Cursor::new(Vec::new());
6910            let bridge_ctx =
6911                BridgeCallContext::new(Box::new(writer), Box::new(reader), "test-session".into());
6912
6913            let bridge = "(function() { globalThis._moduleBridge = true; })()";
6914
6915            // First execution populates cache
6916            {
6917                let ctx = isolate::create_context(&mut iso);
6918                let (code, _, _) = {
6919                    let scope = &mut v8::HandleScope::new(&mut iso);
6920                    let local = v8::Local::new(scope, &ctx);
6921                    let scope = &mut v8::ContextScope::new(scope, local);
6922                    execute_module(
6923                        scope,
6924                        &bridge_ctx,
6925                        bridge,
6926                        "export const a = 1;",
6927                        None,
6928                        &mut cache,
6929                    )
6930                };
6931                assert_eq!(code, 0);
6932                assert!(cache.is_some());
6933            }
6934
6935            // Second execution consumes cache
6936            {
6937                let ctx = isolate::create_context(&mut iso);
6938                let (code, exports, _) = {
6939                    let scope = &mut v8::HandleScope::new(&mut iso);
6940                    let local = v8::Local::new(scope, &ctx);
6941                    let scope = &mut v8::ContextScope::new(scope, local);
6942                    execute_module(
6943                        scope,
6944                        &bridge_ctx,
6945                        bridge,
6946                        "export const b = 2;",
6947                        None,
6948                        &mut cache,
6949                    )
6950                };
6951                assert_eq!(code, 0);
6952                assert!(exports.is_some());
6953                assert!(cache.is_some());
6954            }
6955        }
6956
6957        // Part 64: Empty bridge code does not populate cache
6958        {
6959            let mut iso = isolate::create_isolate(None);
6960            let ctx = isolate::create_context(&mut iso);
6961            let mut cache: Option<BridgeCodeCache> = None;
6962
6963            let (code, _) = {
6964                let scope = &mut v8::HandleScope::new(&mut iso);
6965                let local = v8::Local::new(scope, &ctx);
6966                let scope = &mut v8::ContextScope::new(scope, local);
6967                execute_script(scope, "", "var x = 1;", &mut cache)
6968            };
6969
6970            assert_eq!(code, 0);
6971            assert!(
6972                cache.is_none(),
6973                "cache should not be populated for empty bridge code"
6974            );
6975        }
6976
6977        // Part 65: Batch resolve — multiple imports prefetched in one round-trip
6978        {
6979            let mut iso = isolate::create_isolate(None);
6980            let ctx = isolate::create_context(&mut iso);
6981
6982            let mut response_buf = Vec::new();
6983
6984            // Batch response (call_id=1): two resolved modules
6985            let batch_result = v8_serialize_eval(
6986                &mut iso,
6987                &ctx,
6988                "[{resolved: '/a.mjs', source: 'export const a = 1;'}, {resolved: '/b.mjs', source: 'export const b = 2;'}]",
6989            );
6990            crate::ipc_binary::write_frame(
6991                &mut response_buf,
6992                &crate::ipc_binary::BinaryFrame::BridgeResponse {
6993                    session_id: String::new(),
6994                    call_id: 1,
6995                    status: 0,
6996                    payload: batch_result,
6997                },
6998            )
6999            .unwrap();
7000            crate::ipc_binary::write_frame(
7001                &mut response_buf,
7002                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7003                    session_id: String::new(),
7004                    call_id: 2,
7005                    status: 0,
7006                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7007                },
7008            )
7009            .unwrap();
7010            crate::ipc_binary::write_frame(
7011                &mut response_buf,
7012                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7013                    session_id: String::new(),
7014                    call_id: 3,
7015                    status: 0,
7016                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7017                },
7018            )
7019            .unwrap();
7020
7021            let writer_buf = Arc::new(Mutex::new(Vec::new()));
7022            let bridge_ctx = BridgeCallContext::new(
7023                Box::new(SharedWriter(Arc::clone(&writer_buf))),
7024                Box::new(Cursor::new(response_buf)),
7025                "test-session".into(),
7026            );
7027
7028            let user_code = "import { a } from './a.mjs';\nimport { b } from './b.mjs';\nexport const sum = a + b;";
7029            let (code, exports, error) = {
7030                let scope = &mut v8::HandleScope::new(&mut iso);
7031                let local = v8::Local::new(scope, &ctx);
7032                let scope = &mut v8::ContextScope::new(scope, local);
7033                execute_module(
7034                    scope,
7035                    &bridge_ctx,
7036                    "",
7037                    user_code,
7038                    Some("/app/main.mjs"),
7039                    &mut None,
7040                )
7041            };
7042
7043            assert_eq!(code, 0, "error: {:?}", error);
7044            assert!(error.is_none());
7045            let exports = exports.unwrap();
7046            {
7047                let scope = &mut v8::HandleScope::new(&mut iso);
7048                let local = v8::Local::new(scope, &ctx);
7049                let scope = &mut v8::ContextScope::new(scope, local);
7050                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7051                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7052                let k = v8::String::new(scope, "sum").unwrap();
7053                assert_eq!(
7054                    obj.get(scope, k.into())
7055                        .unwrap()
7056                        .int32_value(scope)
7057                        .unwrap(),
7058                    3
7059                );
7060            }
7061
7062            // Verify only one BridgeCall was sent (the batch call, not individual calls)
7063            let written = writer_buf.lock().unwrap();
7064            let call = crate::ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
7065            match call {
7066                crate::ipc_binary::BinaryFrame::BridgeCall { method, .. } => {
7067                    assert_eq!(method, "_batchResolveModules");
7068                }
7069                _ => panic!("expected BridgeCall for _batchResolveModules"),
7070            }
7071        }
7072
7073        // Part 66: Batch resolve — fallback to individual resolution when batch fails
7074        {
7075            let mut iso = isolate::create_isolate(None);
7076            let ctx = isolate::create_context(&mut iso);
7077
7078            let mut response_buf = Vec::new();
7079
7080            // Batch response (call_id=1): error (simulating unsupported batch method)
7081            crate::ipc_binary::write_frame(
7082                &mut response_buf,
7083                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7084                    session_id: String::new(),
7085                    call_id: 1,
7086                    status: 1,
7087                    payload: "No handler for bridge method: _batchResolveModules"
7088                        .as_bytes()
7089                        .to_vec(),
7090                },
7091            )
7092            .unwrap();
7093
7094            // Individual fallback: _resolveModule (call_id=2) returns "/dep.mjs"
7095            let resolve_result = v8_serialize_str(&mut iso, &ctx, "/dep.mjs");
7096            crate::ipc_binary::write_frame(
7097                &mut response_buf,
7098                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7099                    session_id: String::new(),
7100                    call_id: 2,
7101                    status: 0,
7102                    payload: resolve_result,
7103                },
7104            )
7105            .unwrap();
7106
7107            // Individual fallback: _loadFile (call_id=3) returns source
7108            let load_result = v8_serialize_str(&mut iso, &ctx, "export const val = 42;");
7109            crate::ipc_binary::write_frame(
7110                &mut response_buf,
7111                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7112                    session_id: String::new(),
7113                    call_id: 3,
7114                    status: 0,
7115                    payload: load_result,
7116                },
7117            )
7118            .unwrap();
7119            crate::ipc_binary::write_frame(
7120                &mut response_buf,
7121                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7122                    session_id: String::new(),
7123                    call_id: 4,
7124                    status: 0,
7125                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7126                },
7127            )
7128            .unwrap();
7129
7130            let bridge_ctx = BridgeCallContext::new(
7131                Box::new(Vec::new()),
7132                Box::new(Cursor::new(response_buf)),
7133                "test-session".into(),
7134            );
7135
7136            let user_code = "import { val } from './dep.mjs';\nexport const result = val;";
7137            let (code, exports, error) = {
7138                let scope = &mut v8::HandleScope::new(&mut iso);
7139                let local = v8::Local::new(scope, &ctx);
7140                let scope = &mut v8::ContextScope::new(scope, local);
7141                execute_module(
7142                    scope,
7143                    &bridge_ctx,
7144                    "",
7145                    user_code,
7146                    Some("/app/main.mjs"),
7147                    &mut None,
7148                )
7149            };
7150
7151            assert_eq!(code, 0, "error: {:?}", error);
7152            assert!(error.is_none());
7153            let exports = exports.unwrap();
7154            {
7155                let scope = &mut v8::HandleScope::new(&mut iso);
7156                let local = v8::Local::new(scope, &ctx);
7157                let scope = &mut v8::ContextScope::new(scope, local);
7158                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7159                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7160                let k = v8::String::new(scope, "result").unwrap();
7161                assert_eq!(
7162                    obj.get(scope, k.into())
7163                        .unwrap()
7164                        .int32_value(scope)
7165                        .unwrap(),
7166                    42
7167                );
7168            }
7169        }
7170
7171        // Part 67: Batch resolve — nested imports resolved via BFS prefetch
7172        {
7173            let mut iso = isolate::create_isolate(None);
7174            let ctx = isolate::create_context(&mut iso);
7175
7176            let mut response_buf = Vec::new();
7177
7178            // Level 1 batch (call_id=1): root imports ./a.mjs which imports ./b.mjs
7179            let batch1 = v8_serialize_eval(
7180                &mut iso,
7181                &ctx,
7182                "[{resolved: '/a.mjs', source: \"import { b } from './b.mjs'; export const a = b + 1;\"}]",
7183            );
7184            crate::ipc_binary::write_frame(
7185                &mut response_buf,
7186                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7187                    session_id: String::new(),
7188                    call_id: 1,
7189                    status: 0,
7190                    payload: batch1,
7191                },
7192            )
7193            .unwrap();
7194            crate::ipc_binary::write_frame(
7195                &mut response_buf,
7196                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7197                    session_id: String::new(),
7198                    call_id: 2,
7199                    status: 0,
7200                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7201                },
7202            )
7203            .unwrap();
7204
7205            // Level 2 batch (call_id=3): ./b.mjs has no further imports
7206            let batch2 = v8_serialize_eval(
7207                &mut iso,
7208                &ctx,
7209                "[{resolved: '/b.mjs', source: 'export const b = 10;'}]",
7210            );
7211            crate::ipc_binary::write_frame(
7212                &mut response_buf,
7213                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7214                    session_id: String::new(),
7215                    call_id: 3,
7216                    status: 0,
7217                    payload: batch2,
7218                },
7219            )
7220            .unwrap();
7221            crate::ipc_binary::write_frame(
7222                &mut response_buf,
7223                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7224                    session_id: String::new(),
7225                    call_id: 4,
7226                    status: 0,
7227                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7228                },
7229            )
7230            .unwrap();
7231
7232            let bridge_ctx = BridgeCallContext::new(
7233                Box::new(Vec::new()),
7234                Box::new(Cursor::new(response_buf)),
7235                "test-session".into(),
7236            );
7237
7238            let user_code = "import { a } from './a.mjs';\nexport const result = a;";
7239            let (code, exports, error) = {
7240                let scope = &mut v8::HandleScope::new(&mut iso);
7241                let local = v8::Local::new(scope, &ctx);
7242                let scope = &mut v8::ContextScope::new(scope, local);
7243                execute_module(
7244                    scope,
7245                    &bridge_ctx,
7246                    "",
7247                    user_code,
7248                    Some("/app/main.mjs"),
7249                    &mut None,
7250                )
7251            };
7252
7253            assert_eq!(code, 0, "error: {:?}", error);
7254            assert!(error.is_none());
7255            let exports = exports.unwrap();
7256            {
7257                let scope = &mut v8::HandleScope::new(&mut iso);
7258                let local = v8::Local::new(scope, &ctx);
7259                let scope = &mut v8::ContextScope::new(scope, local);
7260                let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7261                let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7262                let k = v8::String::new(scope, "result").unwrap();
7263                assert_eq!(
7264                    obj.get(scope, k.into())
7265                        .unwrap()
7266                        .int32_value(scope)
7267                        .unwrap(),
7268                    11
7269                );
7270            }
7271        }
7272
7273        // Part 68: Batch resolve — module with no imports skips batch call
7274        {
7275            let mut iso = isolate::create_isolate(None);
7276            let ctx = isolate::create_context(&mut iso);
7277
7278            let writer_buf = Arc::new(Mutex::new(Vec::new()));
7279            let bridge_ctx = BridgeCallContext::new(
7280                Box::new(SharedWriter(Arc::clone(&writer_buf))),
7281                Box::new(Cursor::new(Vec::new())),
7282                "test-session".into(),
7283            );
7284
7285            let user_code = "export const x = 42;";
7286            let (code, _exports, error) = {
7287                let scope = &mut v8::HandleScope::new(&mut iso);
7288                let local = v8::Local::new(scope, &ctx);
7289                let scope = &mut v8::ContextScope::new(scope, local);
7290                execute_module(scope, &bridge_ctx, "", user_code, None, &mut None)
7291            };
7292
7293            assert_eq!(code, 0, "error: {:?}", error);
7294            assert!(error.is_none());
7295
7296            // No BridgeCall should have been sent (no imports to resolve)
7297            let written = writer_buf.lock().unwrap();
7298            assert!(
7299                written.is_empty(),
7300                "no IPC calls expected for module with no imports"
7301            );
7302        }
7303
7304        // Part 68a: Batch prefetch extraction is capped per batch
7305        {
7306            let mut iso = isolate::create_isolate(None);
7307            let ctx = isolate::create_context(&mut iso);
7308            let scope = &mut v8::HandleScope::new(&mut iso);
7309            let local = v8::Local::new(scope, &ctx);
7310            let scope = &mut v8::ContextScope::new(scope, local);
7311
7312            let mut source_code = String::new();
7313            for i in 0..(MAX_MODULE_PREFETCH_BATCH_SIZE + 1) {
7314                source_code.push_str(&format!("import './dep-{i}.mjs';\n"));
7315            }
7316            source_code.push_str("export const ok = true;");
7317
7318            let resource = v8::String::new(scope, "/app/main.mjs").unwrap();
7319            let origin = v8::ScriptOrigin::new(
7320                scope,
7321                resource.into(),
7322                0,
7323                0,
7324                false,
7325                -1,
7326                None,
7327                false,
7328                false,
7329                true,
7330                None,
7331            );
7332            let source = v8::String::new(scope, &source_code).unwrap();
7333            let mut compiled = v8::script_compiler::Source::new(source, Some(&origin));
7334            let module = v8::script_compiler::compile_module(scope, &mut compiled).unwrap();
7335
7336            MODULE_RESOLVE_STATE.with(|cell| {
7337                *cell.borrow_mut() = Some(ModuleResolveState {
7338                    bridge_ctx: std::ptr::null(),
7339                    module_names: HashMap::new(),
7340                    module_cache: HashMap::new(),
7341                    guest_reader: None,
7342                });
7343            });
7344            let imports = extract_uncached_imports(scope, module, "/app/main.mjs");
7345            assert_eq!(
7346                imports.len(),
7347                MAX_MODULE_PREFETCH_BATCH_SIZE,
7348                "static import extraction should stop at the prefetch batch cap"
7349            );
7350            clear_module_state();
7351        }
7352
7353        // Part 68b: Module cache insertion refuses to exceed the cache cap
7354        {
7355            let mut iso = isolate::create_isolate(None);
7356            let ctx = isolate::create_context(&mut iso);
7357            let scope = &mut v8::HandleScope::new(&mut iso);
7358            let local = v8::Local::new(scope, &ctx);
7359            let scope = &mut v8::ContextScope::new(scope, local);
7360
7361            let resource = v8::String::new(scope, "/overflow.mjs").unwrap();
7362            let origin = v8::ScriptOrigin::new(
7363                scope,
7364                resource.into(),
7365                0,
7366                0,
7367                false,
7368                -1,
7369                None,
7370                false,
7371                false,
7372                true,
7373                None,
7374            );
7375            let source = v8::String::new(scope, "export const value = 1;").unwrap();
7376            let mut compiled = v8::script_compiler::Source::new(source, Some(&origin));
7377            let module = v8::script_compiler::compile_module(scope, &mut compiled).unwrap();
7378            let global = v8::Global::new(scope, module);
7379
7380            let mut module_cache = HashMap::new();
7381            for i in 0..(MAX_MODULE_RESOLVE_CACHE_ENTRIES - 1) {
7382                module_cache.insert(format!("/cached-{i}.mjs"), global.clone());
7383            }
7384            MODULE_RESOLVE_STATE.with(|cell| {
7385                *cell.borrow_mut() = Some(ModuleResolveState {
7386                    bridge_ctx: std::ptr::null(),
7387                    module_names: HashMap::new(),
7388                    module_cache,
7389                    guest_reader: None,
7390                });
7391            });
7392
7393            assert!(
7394                !cache_resolved_module(
7395                    module,
7396                    global,
7397                    "/overflow.mjs".into(),
7398                    Some(module_request_cache_key("./overflow.mjs", "/app/main.mjs")),
7399                ),
7400                "cache insert should fail instead of exceeding the cache entry cap"
7401            );
7402            let cache_len = MODULE_RESOLVE_STATE.with(|cell| {
7403                cell.borrow()
7404                    .as_ref()
7405                    .expect("module state")
7406                    .module_cache
7407                    .len()
7408            });
7409            assert_eq!(
7410                cache_len,
7411                MAX_MODULE_RESOLVE_CACHE_ENTRIES - 1,
7412                "failed cache insert must not partially insert entries"
7413            );
7414            clear_module_state();
7415        }
7416
7417        // Part 68c: Batch resolve response parsing is bounded to request length
7418        {
7419            let mut iso = isolate::create_isolate(None);
7420            let ctx = isolate::create_context(&mut iso);
7421
7422            let oversized_response = v8_serialize_eval(
7423                &mut iso,
7424                &ctx,
7425                "[{resolved: '/a.mjs', source: 'export const a = 1;'}, {resolved: '/extra.mjs', source: 'export const extra = 1;'}]",
7426            );
7427            let mut response_buf = Vec::new();
7428            crate::ipc_binary::write_frame(
7429                &mut response_buf,
7430                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7431                    session_id: String::new(),
7432                    call_id: 1,
7433                    status: 0,
7434                    payload: oversized_response,
7435                },
7436            )
7437            .unwrap();
7438            let bridge_ctx = BridgeCallContext::new(
7439                Box::new(Vec::new()),
7440                Box::new(Cursor::new(response_buf)),
7441                "test-session".into(),
7442            );
7443
7444            let results = {
7445                let scope = &mut v8::HandleScope::new(&mut iso);
7446                let local = v8::Local::new(scope, &ctx);
7447                let scope = &mut v8::ContextScope::new(scope, local);
7448                batch_resolve_via_ipc(
7449                    scope,
7450                    &bridge_ctx,
7451                    &[("./a.mjs".to_string(), "/app/main.mjs".to_string())],
7452                )
7453                .expect("batch resolve response")
7454            };
7455            assert_eq!(
7456                results.len(),
7457                1,
7458                "batch response parser must not retain entries beyond the request length"
7459            );
7460            assert_eq!(
7461                results[0]
7462                    .as_ref()
7463                    .map(|(resolved, _source)| resolved.as_str()),
7464                Some("/a.mjs")
7465            );
7466
7467            let mut capped_response_buf = Vec::new();
7468            crate::ipc_binary::write_frame(
7469                &mut capped_response_buf,
7470                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7471                    session_id: String::new(),
7472                    call_id: 1,
7473                    status: 0,
7474                    payload: vec![0; MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES + 1],
7475                },
7476            )
7477            .unwrap();
7478            let capped_bridge_ctx = BridgeCallContext::new(
7479                Box::new(Vec::new()),
7480                Box::new(Cursor::new(capped_response_buf)),
7481                "test-session".into(),
7482            );
7483            let capped_result = {
7484                let scope = &mut v8::HandleScope::new(&mut iso);
7485                let local = v8::Local::new(scope, &ctx);
7486                let scope = &mut v8::ContextScope::new(scope, local);
7487                batch_resolve_via_ipc(
7488                    scope,
7489                    &capped_bridge_ctx,
7490                    &[("./large.mjs".to_string(), "/app/main.mjs".to_string())],
7491                )
7492            };
7493            assert!(
7494                capped_result.is_none(),
7495                "batch response payloads over the byte cap should be rejected before deserialization"
7496            );
7497        }
7498
7499        // Part 68d: CJS named export extraction is capped
7500        {
7501            let mut source = String::new();
7502            for i in 0..(MAX_CJS_NAMED_EXPORTS + 1) {
7503                source.push_str(&format!("exports.name{i} = {i};\n"));
7504            }
7505
7506            let exports = extract_cjs_export_names(&source);
7507            assert_eq!(
7508                exports.len(),
7509                MAX_CJS_NAMED_EXPORTS,
7510                "static CJS export extraction should stop at the named export cap"
7511            );
7512            assert!(
7513                !exports.contains(&format!("name{}", MAX_CJS_NAMED_EXPORTS)),
7514                "exports beyond the cap must not be retained"
7515            );
7516
7517            let object_literal_exports =
7518                extract_cjs_export_names("module.exports = { foo: 1, shorthand, default: 2 };");
7519            assert!(
7520                object_literal_exports.contains(&"foo".to_string()),
7521                "module.exports object literal keys should be statically extracted"
7522            );
7523            assert!(
7524                object_literal_exports.contains(&"shorthand".to_string()),
7525                "module.exports shorthand keys should be statically extracted"
7526            );
7527            assert!(
7528                !object_literal_exports.contains(&"default".to_string()),
7529                "default should not be emitted as a named CJS export"
7530            );
7531
7532            let object_assign_exports =
7533                extract_cjs_export_names("Object.assign(module.exports, { bar: 1, baz });");
7534            assert!(
7535                object_assign_exports.contains(&"bar".to_string())
7536                    && object_assign_exports.contains(&"baz".to_string()),
7537                "Object.assign(module.exports, object literal) keys should be extracted"
7538            );
7539
7540            let multiline_exports = extract_cjs_export_names(
7541                r#"
7542                module.exports = {
7543                    multiFoo: 1,
7544                    multiBar,
7545                };
7546
7547                Object.assign(module.exports, {
7548                    multiBaz: 2,
7549                });
7550                "#,
7551            );
7552            assert!(
7553                multiline_exports.contains(&"multiFoo".to_string())
7554                    && multiline_exports.contains(&"multiBar".to_string())
7555                    && multiline_exports.contains(&"multiBaz".to_string()),
7556                "multiline CJS object literal export keys should be extracted"
7557            );
7558
7559            let false_positive_exports = extract_cjs_export_names(
7560                r#"
7561                module.exports.foo = { fakeOne: 1 };
7562                Object.assign(otherTarget, { fakeTwo: 2 });
7563                // module.exports = { fakeThree: 3 };
7564                const text = "Object.assign(module.exports, { fakeFour: 4 })";
7565                /* exports.fakeFive = 5; */
7566                const tpl = `Object.defineProperty(exports, "fakeSix", {})`;
7567                module.exports = { "fake:seven": 7 };
7568                const re = /module.exports = { fakeEight: 8 }/;
7569                function f() { return /module.exports = { fakeNine: 9 }/; }
7570                const g = () => /exports.fakeTen = 10/;
7571                const h = /[/]module.exports = { fakeEleven: 11 }/;
7572                if (ok) /exports.fakeTwelve = 12/.test(input);
7573                if (ok) {} /exports.fakeThirteen = 13/.test(input);
7574                "#,
7575            );
7576            assert!(
7577                !false_positive_exports.contains(&"fakeOne".to_string())
7578                    && !false_positive_exports.contains(&"fakeTwo".to_string())
7579                    && !false_positive_exports.contains(&"fakeThree".to_string())
7580                    && !false_positive_exports.contains(&"fakeFour".to_string())
7581                    && !false_positive_exports.contains(&"fakeFive".to_string())
7582                    && !false_positive_exports.contains(&"fakeSix".to_string())
7583                    && !false_positive_exports.contains(&"fake".to_string())
7584                    && !false_positive_exports.contains(&"fakeEight".to_string())
7585                    && !false_positive_exports.contains(&"fakeNine".to_string())
7586                    && !false_positive_exports.contains(&"fakeTen".to_string())
7587                    && !false_positive_exports.contains(&"fakeEleven".to_string())
7588                    && !false_positive_exports.contains(&"fakeTwelve".to_string())
7589                    && !false_positive_exports.contains(&"fakeThirteen".to_string()),
7590                "object literal extraction should not emit keys from unrelated objects"
7591            );
7592
7593            let mut malformed_literals = String::new();
7594            for i in 0..2048 {
7595                malformed_literals.push_str(&format!("module.exports = {{ fake{i}: "));
7596            }
7597            let malformed_exports = extract_cjs_export_names(&malformed_literals);
7598            assert!(
7599                malformed_exports.is_empty(),
7600                "malformed object literals should be skipped without collecting fake keys"
7601            );
7602
7603            let regex_value_exports =
7604                extract_cjs_export_names("module.exports = { real: /}/, alsoReal: /[,]}/ };");
7605            assert!(
7606                regex_value_exports.contains(&"real".to_string())
7607                    && regex_value_exports.contains(&"alsoReal".to_string()),
7608                "regex values inside CJS object literals should not terminate the object scan"
7609            );
7610
7611            let division_exports = extract_cjs_export_names("const n = 4 / 2; exports.after = n;");
7612            assert!(
7613                division_exports.contains(&"after".to_string()),
7614                "ordinary division should not hide later CJS export assignments"
7615            );
7616
7617            let reserved_exports = extract_cjs_export_names(
7618                r#"
7619                exports.arguments = 1;
7620                exports.class = 1;
7621                module.exports = { await: 2 };
7622                module.exports = { let: 3, static: 4, eval: 5 };
7623                Object.assign(module.exports, {
7624                    implements: 6,
7625                    interface: 7,
7626                    package: 8,
7627                    private: 9,
7628                    protected: 10,
7629                    public: 11,
7630                });
7631                Object.defineProperty(exports, "return", {});
7632                "#,
7633            );
7634            assert!(
7635                reserved_exports.is_empty(),
7636                "reserved words should not be emitted as generated ESM bindings"
7637            );
7638
7639            let mut huge_literal = String::from("module.exports = {\n");
7640            for i in 0..(MAX_CJS_NAMED_EXPORTS + 1) {
7641                huge_literal.push_str(&format!("literalName{i}: {i},\n"));
7642            }
7643            huge_literal.push_str("};");
7644            let huge_literal_exports = extract_cjs_export_names(&huge_literal);
7645            assert_eq!(
7646                huge_literal_exports.len(),
7647                MAX_CJS_NAMED_EXPORTS,
7648                "object literal export extraction should stop at the named export cap"
7649            );
7650            assert!(
7651                !huge_literal_exports.contains(&format!("literalName{}", MAX_CJS_NAMED_EXPORTS)),
7652                "object literal exports beyond the cap must not be retained"
7653            );
7654
7655            let mut iso = isolate::create_isolate(None);
7656            let ctx = isolate::create_context(&mut iso);
7657            let scope = &mut v8::HandleScope::new(&mut iso);
7658            let local = v8::Local::new(scope, &ctx);
7659            let scope = &mut v8::ContextScope::new(scope, local);
7660            let shim =
7661                build_cjs_esm_shim(scope, "module.exports = { foo: 1 };", "/object-literal.cjs");
7662            assert!(
7663                shim.contains("export const foo = _cjsModule[\"foo\"];"),
7664                "CJS shim should preserve statically extractable named exports"
7665            );
7666        }
7667
7668        // Part 68e: CJS shim degrades to default-only when runtime extraction is unavailable
7669        {
7670            let mut iso = isolate::create_isolate(None);
7671            let ctx = isolate::create_context(&mut iso);
7672            let scope = &mut v8::HandleScope::new(&mut iso);
7673            let local = v8::Local::new(scope, &ctx);
7674            let scope = &mut v8::ContextScope::new(scope, local);
7675
7676            let shim = build_cjs_esm_shim(
7677                scope,
7678                "module.exports = makeExportsDynamically();",
7679                "/runtime.cjs",
7680            );
7681
7682            assert!(
7683                shim.contains("export default _cjsModule;"),
7684                "CJS shim should preserve default import support"
7685            );
7686            assert!(
7687                !shim.contains("export const name0"),
7688                "CJS shim must degrade to default-only when runtime extraction is unavailable"
7689            );
7690        }
7691
7692        // Part 68f: CJS shim runtime fallback enumerates dynamically computed exports
7693        {
7694            let mut iso = isolate::create_isolate(None);
7695            let ctx = isolate::create_context(&mut iso);
7696            let scope = &mut v8::HandleScope::new(&mut iso);
7697            let local = v8::Local::new(scope, &ctx);
7698            let scope = &mut v8::ContextScope::new(scope, local);
7699
7700            let setup = v8::String::new(
7701                scope,
7702                "globalThis._requireFrom = function (path, referrer) { return { dynamicA: 1, dynamicB: 2, default: 3, __esModule: true }; };",
7703            )
7704            .unwrap();
7705            let script = v8::Script::compile(scope, setup, None).unwrap();
7706            script.run(scope).unwrap();
7707
7708            let shim = build_cjs_esm_shim(
7709                scope,
7710                "module.exports = makeExportsDynamically();",
7711                "/dynamic.cjs",
7712            );
7713
7714            assert!(
7715                shim.contains("export const dynamicA = _cjsModule[\"dynamicA\"];"),
7716                "runtime fallback should surface dynamically computed named exports"
7717            );
7718            assert!(
7719                shim.contains("export const dynamicB = _cjsModule[\"dynamicB\"];"),
7720                "runtime fallback should surface every dynamically computed named export"
7721            );
7722            assert!(
7723                shim.contains("export default _cjsModule;"),
7724                "CJS shim should preserve default import support"
7725            );
7726            assert!(
7727                !shim.contains("export const default"),
7728                "runtime fallback must not emit a named export for default"
7729            );
7730            assert!(
7731                !shim.contains("__esModule"),
7732                "runtime fallback must not emit a named export for __esModule"
7733            );
7734        }
7735
7736        // Part 68g: CJS shim runtime fallback bounds export count and name length
7737        {
7738            let mut iso = isolate::create_isolate(None);
7739            let ctx = isolate::create_context(&mut iso);
7740            let scope = &mut v8::HandleScope::new(&mut iso);
7741            let local = v8::Local::new(scope, &ctx);
7742            let scope = &mut v8::ContextScope::new(scope, local);
7743
7744            let setup = v8::String::new(
7745                scope,
7746                "globalThis._requireFrom = function () { const o = {}; for (let i = 0; i < 1025; i++) o[\"k\" + String(i).padStart(4, \"0\")] = i; o[\"x\".repeat(600)] = 1; return o; };",
7747            )
7748            .unwrap();
7749            let script = v8::Script::compile(scope, setup, None).unwrap();
7750            script.run(scope).unwrap();
7751
7752            let shim = build_cjs_esm_shim(
7753                scope,
7754                "module.exports = makeExportsDynamically();",
7755                "/bounded.cjs",
7756            );
7757
7758            let export_count = shim.matches("export const ").count();
7759            assert_eq!(
7760                export_count, MAX_CJS_NAMED_EXPORTS,
7761                "runtime fallback should stop collecting names at the named export cap"
7762            );
7763            assert!(
7764                !shim.contains("export const k1024"),
7765                "runtime fallback exports beyond the cap must not be retained"
7766            );
7767            let longest_export_name = shim
7768                .lines()
7769                .filter_map(|line| line.strip_prefix("export const "))
7770                .filter_map(|rest| rest.split(' ').next())
7771                .map(str::len)
7772                .max()
7773                .unwrap_or(0);
7774            assert!(
7775                longest_export_name <= MAX_CJS_RUNTIME_EXPORT_NAME_LEN,
7776                "runtime fallback must skip export names longer than the length cap"
7777            );
7778        }
7779
7780        // Part 68h: CJS shim runtime fallback tolerates guest evaluation failure
7781        {
7782            let mut iso = isolate::create_isolate(None);
7783            let ctx = isolate::create_context(&mut iso);
7784            let scope = &mut v8::HandleScope::new(&mut iso);
7785            let local = v8::Local::new(scope, &ctx);
7786            let scope = &mut v8::ContextScope::new(scope, local);
7787
7788            let setup = v8::String::new(
7789                scope,
7790                "globalThis._requireFrom = function () { throw new Error(\"boom\"); };",
7791            )
7792            .unwrap();
7793            let script = v8::Script::compile(scope, setup, None).unwrap();
7794            script.run(scope).unwrap();
7795
7796            let shim = build_cjs_esm_shim(
7797                scope,
7798                "module.exports = makeExportsDynamically();",
7799                "/throwing.cjs",
7800            );
7801
7802            assert!(
7803                shim.contains("export default _cjsModule;"),
7804                "CJS shim should preserve default import support after a guest throw"
7805            );
7806            assert!(
7807                !shim.contains("export const "),
7808                "runtime fallback should yield no named exports when module evaluation throws"
7809            );
7810        }
7811
7812        // Part 69: Dynamic import works after execute_module returns
7813        {
7814            let mut iso = isolate::create_isolate(None);
7815            iso.set_host_import_module_dynamically_callback(dynamic_import_callback);
7816            iso.set_host_initialize_import_meta_object_callback(import_meta_object_callback);
7817            let ctx = isolate::create_context(&mut iso);
7818
7819            let mut response_buf = Vec::new();
7820
7821            let resolve_result = v8_serialize_str(&mut iso, &ctx, "/dep.mjs");
7822            crate::ipc_binary::write_frame(
7823                &mut response_buf,
7824                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7825                    session_id: String::new(),
7826                    call_id: 1,
7827                    status: 0,
7828                    payload: resolve_result,
7829                },
7830            )
7831            .unwrap();
7832
7833            let load_result = v8_serialize_str(&mut iso, &ctx, "export const value = 42;");
7834            crate::ipc_binary::write_frame(
7835                &mut response_buf,
7836                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7837                    session_id: String::new(),
7838                    call_id: 2,
7839                    status: 0,
7840                    payload: load_result,
7841                },
7842            )
7843            .unwrap();
7844            crate::ipc_binary::write_frame(
7845                &mut response_buf,
7846                &crate::ipc_binary::BinaryFrame::BridgeResponse {
7847                    session_id: String::new(),
7848                    call_id: 3,
7849                    status: 0,
7850                    payload: v8_serialize_str(&mut iso, &ctx, "module"),
7851                },
7852            )
7853            .unwrap();
7854
7855            let bridge_ctx = BridgeCallContext::new(
7856                Box::new(Vec::new()),
7857                Box::new(Cursor::new(response_buf)),
7858                "test-session".into(),
7859            );
7860
7861            let user_code = r#"
7862                globalThis.loadDep = async () => (await import("./dep.mjs")).value;
7863                export const ready = true;
7864            "#;
7865            let (code, exports, error) = {
7866                let scope = &mut v8::HandleScope::new(&mut iso);
7867                let local = v8::Local::new(scope, &ctx);
7868                let scope = &mut v8::ContextScope::new(scope, local);
7869                execute_module(
7870                    scope,
7871                    &bridge_ctx,
7872                    "",
7873                    user_code,
7874                    Some("/app/main.mjs"),
7875                    &mut None,
7876                )
7877            };
7878
7879            assert_eq!(code, 0, "error: {:?}", error);
7880            assert!(error.is_none());
7881            assert!(exports.is_some());
7882
7883            {
7884                let scope = &mut v8::HandleScope::new(&mut iso);
7885                let local = v8::Local::new(scope, &ctx);
7886                let scope = &mut v8::ContextScope::new(scope, local);
7887                let tc = &mut v8::TryCatch::new(scope);
7888                let source = v8::String::new(
7889                    tc,
7890                    "globalThis.__depPromise = globalThis.loadDep().then((value) => { globalThis.__depValue = value; return value; });",
7891                )
7892                .unwrap();
7893                let script = v8::Script::compile(tc, source, None).unwrap();
7894                assert!(script.run(tc).is_some());
7895                tc.perform_microtask_checkpoint();
7896                assert!(tc.exception().is_none());
7897            }
7898
7899            assert_eq!(eval(&mut iso, &ctx, "String(globalThis.__depValue)"), "42");
7900            clear_module_state();
7901        }
7902    }
7903}