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