1use std::collections::HashMap;
4use std::io::{Read, Write};
5use std::process::{Command, Stdio};
6use std::sync::{Arc, Condvar, Mutex};
7
8use sha2::{Digest, Sha256};
9
10use crate::bridge::{external_refs, register_stub_bridge_fns};
11use crate::isolate::init_v8_platform;
12use crate::session::{async_bridge_fns, sync_bridge_fns};
13
14const MAX_SNAPSHOT_BLOB_BYTES: usize = 50 * 1024 * 1024;
17const MAX_V8_BRIDGE_CODE_BYTES: usize = 16 * 1024 * 1024;
18const MAX_V8_USERLAND_CODE_BYTES: usize = 32 * 1024 * 1024;
22pub(crate) const V8_BRIDGE_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_BRIDGE_CODE_LIMIT";
23pub(crate) const V8_USERLAND_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_USERLAND_CODE_LIMIT";
24
25const SNAPSHOT_HELPER_ENV: &str = "AGENTOS_V8_SNAPSHOT_HELPER";
26const SNAPSHOT_HELPER_MAGIC: &[u8; 8] = b"SEV8SNP1";
27const SNAPSHOT_HELPER_OK: &[u8; 2] = b"OK";
28const SNAPSHOT_HELPER_ERR: &[u8; 3] = b"ERR";
29const SNAPSHOT_HELPER_NONE_LEN: u64 = u64::MAX;
30
31#[cfg(any(target_os = "linux", target_os = "android"))]
32#[used]
33#[link_section = ".init_array"]
34static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
35
36#[cfg(target_os = "macos")]
40#[used]
41#[link_section = "__DATA,__mod_init_func"]
42static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
43
44#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
45extern "C" fn snapshot_helper_ctor() {
46 if std::env::var_os(SNAPSHOT_HELPER_ENV).is_some() {
47 let code = run_snapshot_helper_from_stdio();
48 std::process::exit(code);
49 }
50}
51
52pub(crate) fn validate_bridge_code_size(bridge_code: &str) -> Result<(), String> {
53 if bridge_code.len() > MAX_V8_BRIDGE_CODE_BYTES {
54 return Err(format!(
55 "{V8_BRIDGE_CODE_LIMIT_ERROR_CODE}: bridge code too large for V8 bridge setup: {} bytes (max {})",
56 bridge_code.len(),
57 MAX_V8_BRIDGE_CODE_BYTES
58 ));
59 }
60
61 Ok(())
62}
63
64pub(crate) fn validate_userland_code_size(userland_code: &str) -> Result<(), String> {
65 if userland_code.len() > MAX_V8_USERLAND_CODE_BYTES {
66 return Err(format!(
67 "{V8_USERLAND_CODE_LIMIT_ERROR_CODE}: userland snapshot code too large: {} bytes (max {})",
68 userland_code.len(),
69 MAX_V8_USERLAND_CODE_BYTES
70 ));
71 }
72
73 Ok(())
74}
75
76const SNAPSHOT_USERLAND_PREP: &str = r#"
82(function () {
83 // Agent-SDK bundles are esbuild IIFEs that expect a global CJS `require` for
84 // their node-builtin imports, but the bridge exposes require only via module
85 // wrappers. Bind one from the bridge's namespaced createRequire so the bundle's
86 // __require resolves builtins (via the in-context loadBuiltinModule) during
87 // snapshot eval; it also works post-restore (resolution flows through the real
88 // bridge fns swapped in after restore).
89 if (typeof globalThis.require === "undefined" &&
90 typeof globalThis.__secureExecGuestCreateRequire === "function") {
91 try {
92 globalThis.require = globalThis.__secureExecGuestCreateRequire("/root/index.js");
93 } catch (e) {}
94 }
95 // `process.versions` is a bridge-backed lazy getter: it derives `.node` from the
96 // per-session `config2.version` and merges a host bridge call. During snapshot
97 // creation the bridge fns are stubs AND the default `_processConfig` has no
98 // `version`, so the live getter THROWS — an agent SDK that reads it at
99 // module-init would fail. Rather than REPLACE the getter with a static value
100 // (which would permanently shadow the real per-session version for every
101 // restored session — they'd all read a frozen, fabricated identity), WRAP it:
102 // defer to the live getter so that post-restore — once the real bridge fns and
103 // per-session config are injected — sessions read accurate per-session versions,
104 // and fall back to the static, snapshot-safe identity only while the live getter
105 // throws (i.e. during snapshot creation, before any real config exists).
106 if (typeof process !== "undefined" && process) {
107 var staticVersions = { node: "20.0.0", v8: "12.0.0", uv: "1.0.0", modules: "115" };
108 try {
109 var __verDesc = Object.getOwnPropertyDescriptor(process, "versions");
110 var __liveVersions = __verDesc && __verDesc.get;
111 Object.defineProperty(process, "versions", {
112 configurable: true,
113 enumerable: true,
114 get: function () {
115 if (__liveVersions) {
116 try {
117 var v = __liveVersions.call(this);
118 // A real per-session result has a node version; during
119 // snapshot creation the live getter throws before here.
120 if (v && typeof v === "object" && v.node) return v;
121 } catch (e) {}
122 }
123 return staticVersions;
124 },
125 });
126 } catch (e) {}
127 }
128})();
129"#;
130
131fn run_snapshot_script(
135 scope: &mut v8::HandleScope,
136 code: &str,
137 label: &str,
138 eager_compile: bool,
139) -> Result<(), String> {
140 let try_catch = &mut v8::TryCatch::new(scope);
141 let source = match v8::String::new(try_catch, code) {
142 Some(source) => source,
143 None => return Err(format!("failed to create V8 string for {label}")),
144 };
145 let script = if eager_compile {
152 let resource_name = v8::String::new(try_catch, label).unwrap();
153 let origin = v8::ScriptOrigin::new(
154 try_catch,
155 resource_name.into(),
156 0,
157 0,
158 false,
159 0,
160 None,
161 false,
162 false,
163 false,
164 None,
165 );
166 let mut source = v8::script_compiler::Source::new(source, Some(&origin));
167 v8::script_compiler::compile(
168 try_catch,
169 &mut source,
170 v8::script_compiler::CompileOptions::EagerCompile,
171 v8::script_compiler::NoCacheReason::NoReason,
172 )
173 } else {
174 v8::Script::compile(try_catch, source, None)
175 };
176 let Some(script) = script else {
177 let message = try_catch
178 .exception()
179 .map(|exception| exception.to_rust_string_lossy(try_catch))
180 .unwrap_or_else(|| format!("{label} compilation failed during snapshot creation"));
181 return Err(format!(
182 "{label} compilation failed during snapshot creation: {message}"
183 ));
184 };
185 if script.run(try_catch).is_none() {
186 let message = try_catch
187 .exception()
188 .map(|exception| exception.to_rust_string_lossy(try_catch))
189 .unwrap_or_else(|| format!("{label} execution failed during snapshot creation"));
190 return Err(format!(
191 "{label} execution failed during snapshot creation: {message}"
192 ));
193 }
194 Ok(())
195}
196
197pub fn create_snapshot(bridge_code: &str) -> Result<Vec<u8>, String> {
209 create_snapshot_inner(bridge_code, None)
210}
211
212pub fn create_snapshot_with_userland(
225 bridge_code: &str,
226 userland_code: &str,
227) -> Result<Vec<u8>, String> {
228 create_snapshot_inner(bridge_code, Some(userland_code))
229}
230
231fn create_snapshot_inner(
232 bridge_code: &str,
233 userland_code: Option<&str>,
234) -> Result<Vec<u8>, String> {
235 validate_bridge_code_size(bridge_code)?;
236 if let Some(userland_code) = userland_code {
237 validate_userland_code_size(userland_code)?;
238 }
239
240 create_snapshot_in_subprocess(bridge_code, userland_code)
241}
242
243fn create_snapshot_inner_in_process(
244 bridge_code: &str,
245 userland_code: Option<&str>,
246) -> Result<Vec<u8>, String> {
247 validate_bridge_code_size(bridge_code)?;
248 if let Some(userland_code) = userland_code {
249 validate_userland_code_size(userland_code)?;
250 }
251
252 init_v8_platform();
253 crate::isolate::prepare_current_thread();
254
255 crate::isolate::with_isolate_lifecycle_lock(|| {
256 let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
257 let bridge_result = {
258 let scope = &mut v8::HandleScope::new(&mut isolate);
259 let context = v8::Context::new(scope, Default::default());
260 let scope = &mut v8::ContextScope::new(scope, context);
261
262 let sync_bridge_fns = sync_bridge_fns();
264 let async_bridge_fns = async_bridge_fns();
265 register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
266
267 inject_snapshot_defaults(scope);
269
270 let result = (|| -> Result<(), String> {
274 run_snapshot_script(scope, bridge_code, "bridge code", false)?;
275 if let Some(userland_code) = userland_code {
276 run_snapshot_script(scope, SNAPSHOT_USERLAND_PREP, "userland prep", false)?;
282 run_snapshot_script(scope, userland_code, "userland code", true)?;
283 }
284 Ok(())
285 })();
286
287 scope.set_default_context(context);
288 result
289 };
290 let blob = isolate
291 .create_blob(v8::FunctionCodeHandling::Keep)
292 .ok_or_else(|| "V8 snapshot creation failed".to_string())?;
293 bridge_result?;
294
295 if blob.len() > MAX_SNAPSHOT_BLOB_BYTES {
297 return Err(format!(
298 "snapshot blob too large: {} bytes (max {})",
299 blob.len(),
300 MAX_SNAPSHOT_BLOB_BYTES
301 ));
302 }
303
304 Ok(blob.to_vec())
305 })
306}
307
308fn create_snapshot_in_subprocess(
309 bridge_code: &str,
310 userland_code: Option<&str>,
311) -> Result<Vec<u8>, String> {
312 let current_exe =
313 std::env::current_exe().map_err(|error| format!("snapshot helper path: {error}"))?;
314 let mut child = Command::new(current_exe)
315 .env(SNAPSHOT_HELPER_ENV, "1")
316 .stdin(Stdio::piped())
317 .stdout(Stdio::piped())
318 .stderr(Stdio::piped())
319 .spawn()
320 .map_err(|error| format!("spawn snapshot helper: {error}"))?;
321
322 {
323 let mut stdin = child
324 .stdin
325 .take()
326 .ok_or_else(|| String::from("snapshot helper stdin unavailable"))?;
327 write_snapshot_helper_request(&mut stdin, bridge_code, userland_code)?;
328 }
329
330 let output = child
331 .wait_with_output()
332 .map_err(|error| format!("wait for snapshot helper: {error}"))?;
333 if !output.status.success() {
334 let stderr = String::from_utf8_lossy(&output.stderr);
335 return Err(format!(
336 "snapshot helper exited with status {}: {}",
337 output.status, stderr
338 ));
339 }
340
341 parse_snapshot_helper_response(&output.stdout, &output.stderr)
342}
343
344fn write_snapshot_helper_request(
345 writer: &mut impl Write,
346 bridge_code: &str,
347 userland_code: Option<&str>,
348) -> Result<(), String> {
349 writer
350 .write_all(SNAPSHOT_HELPER_MAGIC)
351 .map_err(|error| format!("write snapshot helper magic: {error}"))?;
352 write_u64(writer, bridge_code.len() as u64)?;
353 write_u64(
354 writer,
355 userland_code
356 .map(|code| code.len() as u64)
357 .unwrap_or(SNAPSHOT_HELPER_NONE_LEN),
358 )?;
359 writer
360 .write_all(bridge_code.as_bytes())
361 .map_err(|error| format!("write snapshot helper bridge code: {error}"))?;
362 if let Some(userland_code) = userland_code {
363 writer
364 .write_all(userland_code.as_bytes())
365 .map_err(|error| format!("write snapshot helper userland code: {error}"))?;
366 }
367 Ok(())
368}
369
370fn parse_snapshot_helper_response(stdout: &[u8], stderr: &[u8]) -> Result<Vec<u8>, String> {
371 if stdout.starts_with(SNAPSHOT_HELPER_OK) {
372 let payload = &stdout[SNAPSHOT_HELPER_OK.len()..];
373 let (len, rest) = read_u64_from_slice(payload)?;
374 let len = usize::try_from(len)
375 .map_err(|_| String::from("snapshot helper OK payload length overflows usize"))?;
376 if rest.len() != len {
377 return Err(format!(
378 "snapshot helper OK payload length mismatch: declared {}, got {}",
379 len,
380 rest.len()
381 ));
382 }
383 return Ok(rest.to_vec());
384 }
385
386 if stdout.starts_with(SNAPSHOT_HELPER_ERR) {
387 let payload = &stdout[SNAPSHOT_HELPER_ERR.len()..];
388 let (len, rest) = read_u64_from_slice(payload)?;
389 let len = usize::try_from(len)
390 .map_err(|_| String::from("snapshot helper error length overflows usize"))?;
391 if rest.len() != len {
392 return Err(format!(
393 "snapshot helper error length mismatch: declared {}, got {}",
394 len,
395 rest.len()
396 ));
397 }
398 let message = String::from_utf8_lossy(rest);
399 return Err(message.into_owned());
400 }
401
402 Err(format!(
403 "snapshot helper returned invalid response (stdout {} bytes, stderr: {})",
404 stdout.len(),
405 String::from_utf8_lossy(stderr)
406 ))
407}
408
409fn run_snapshot_helper_from_stdio() -> i32 {
410 let mut input = Vec::new();
411 if let Err(error) = std::io::stdin().read_to_end(&mut input) {
412 let _ = write_snapshot_helper_error(format!("read snapshot helper request: {error}"));
413 return 2;
414 }
415
416 let result = (|| -> Result<Vec<u8>, String> {
417 let (bridge_code, userland_code) = parse_snapshot_helper_request(&input)?;
418 create_snapshot_inner_in_process(&bridge_code, userland_code.as_deref())
419 })();
420
421 match result {
422 Ok(blob) => match write_snapshot_helper_ok(&blob) {
423 Ok(()) => 0,
424 Err(error) => {
425 eprintln!("write snapshot helper response: {error}");
426 2
427 }
428 },
429 Err(error) => match write_snapshot_helper_error(error) {
430 Ok(()) => 0,
431 Err(error) => {
432 eprintln!("write snapshot helper error response: {error}");
433 2
434 }
435 },
436 }
437}
438
439fn parse_snapshot_helper_request(input: &[u8]) -> Result<(String, Option<String>), String> {
440 let Some(rest) = input.strip_prefix(SNAPSHOT_HELPER_MAGIC) else {
441 return Err(String::from("snapshot helper request missing magic"));
442 };
443 let (bridge_len, rest) = read_u64_from_slice(rest)?;
444 let (userland_len, rest) = read_u64_from_slice(rest)?;
445 let bridge_len = usize::try_from(bridge_len)
446 .map_err(|_| String::from("snapshot helper bridge length overflows usize"))?;
447 if rest.len() < bridge_len {
448 return Err(format!(
449 "snapshot helper bridge length mismatch: declared {}, got {}",
450 bridge_len,
451 rest.len()
452 ));
453 }
454 let (bridge_bytes, rest) = rest.split_at(bridge_len);
455 let userland_code = if userland_len == SNAPSHOT_HELPER_NONE_LEN {
456 if !rest.is_empty() {
457 return Err(format!(
458 "snapshot helper request has {} trailing bytes after bridge-only payload",
459 rest.len()
460 ));
461 }
462 None
463 } else {
464 let userland_len = usize::try_from(userland_len)
465 .map_err(|_| String::from("snapshot helper userland length overflows usize"))?;
466 if rest.len() != userland_len {
467 return Err(format!(
468 "snapshot helper userland length mismatch: declared {}, got {}",
469 userland_len,
470 rest.len()
471 ));
472 }
473 Some(
474 String::from_utf8(rest.to_vec())
475 .map_err(|error| format!("snapshot helper userland is not UTF-8: {error}"))?,
476 )
477 };
478 let bridge_code = String::from_utf8(bridge_bytes.to_vec())
479 .map_err(|error| format!("snapshot helper bridge is not UTF-8: {error}"))?;
480 Ok((bridge_code, userland_code))
481}
482
483fn write_snapshot_helper_ok(blob: &[u8]) -> std::io::Result<()> {
484 let mut stdout = std::io::stdout().lock();
485 stdout.write_all(SNAPSHOT_HELPER_OK)?;
486 write_u64_io(&mut stdout, blob.len() as u64)?;
487 stdout.write_all(blob)?;
488 stdout.flush()
489}
490
491fn write_snapshot_helper_error(message: String) -> std::io::Result<()> {
492 let mut stdout = std::io::stdout().lock();
493 stdout.write_all(SNAPSHOT_HELPER_ERR)?;
494 write_u64_io(&mut stdout, message.len() as u64)?;
495 stdout.write_all(message.as_bytes())?;
496 stdout.flush()
497}
498
499fn write_u64(writer: &mut impl Write, value: u64) -> Result<(), String> {
500 write_u64_io(writer, value).map_err(|error| format!("write snapshot helper length: {error}"))
501}
502
503fn write_u64_io(writer: &mut impl Write, value: u64) -> std::io::Result<()> {
504 writer.write_all(&value.to_le_bytes())
505}
506
507fn read_u64_from_slice(input: &[u8]) -> Result<(u64, &[u8]), String> {
508 let bytes = input
509 .get(..8)
510 .ok_or_else(|| String::from("snapshot helper payload ended before u64"))?;
511 let mut value = [0_u8; 8];
512 value.copy_from_slice(bytes);
513 Ok((u64::from_le_bytes(value), &input[8..]))
514}
515
516fn inject_snapshot_defaults(scope: &mut v8::HandleScope) {
526 let context = scope.get_current_context();
527 let global = context.global(scope);
528
529 let pc_code = r#"({
531 cwd: "/",
532 env: {},
533 timing_mitigation: "off",
534 frozen_time_ms: null,
535 high_resolution_time: false
536 })"#;
537 let pc_source = v8::String::new(scope, pc_code).unwrap();
538 let pc_script = v8::Script::compile(scope, pc_source, None).unwrap();
539 let pc_val = pc_script.run(scope).unwrap();
540 if let Some(pc_obj) = pc_val.to_object(scope) {
541 pc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
542 }
543 let pc_key = v8::String::new(scope, "_processConfig").unwrap();
544 let attr = v8::PropertyAttribute::READ_ONLY;
547 global.define_own_property(scope, pc_key.into(), pc_val, attr);
548
549 let oc_code = r#"({
551 homedir: "/root",
552 tmpdir: "/tmp",
553 platform: "linux",
554 arch: "x64"
555 })"#;
556 let oc_source = v8::String::new(scope, oc_code).unwrap();
557 let oc_script = v8::Script::compile(scope, oc_source, None).unwrap();
558 let oc_val = oc_script.run(scope).unwrap();
559 if let Some(oc_obj) = oc_val.to_object(scope) {
560 oc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
561 }
562 let oc_key = v8::String::new(scope, "_osConfig").unwrap();
563 let attr2 = v8::PropertyAttribute::READ_ONLY;
565 global.define_own_property(scope, oc_key.into(), oc_val, attr2);
566}
567
568pub fn create_isolate_from_snapshot<B>(blob: B, heap_limit_mb: Option<u32>) -> v8::OwnedIsolate
577where
578 B: std::ops::Deref<Target = [u8]> + std::borrow::Borrow<[u8]> + 'static,
579{
580 init_v8_platform();
581 crate::isolate::prepare_current_thread();
582
583 let limit = heap_limit_mb.unwrap_or(crate::isolate::DEFAULT_HEAP_LIMIT_MB);
586 let limit_bytes = (limit as usize) * 1024 * 1024;
587 let params = v8::CreateParams::default()
588 .snapshot_blob(blob)
589 .external_references(&**external_refs())
590 .heap_limits(0, limit_bytes);
591 let mut isolate = crate::isolate::with_isolate_lifecycle_lock(|| v8::Isolate::new(params));
592 crate::isolate::configure_isolate(&mut isolate);
593 crate::isolate::install_heap_limit_guard(&mut isolate);
596 isolate
597}
598
599pub type SnapshotCacheKey = [u8; 32];
600
601pub struct SnapshotCache {
608 inner: Mutex<CacheInner>,
609 max_entries: usize,
610}
611
612struct CacheInner {
613 entries: Vec<CacheEntry>,
614 in_flight: HashMap<SnapshotCacheKey, Arc<InFlightEntry>>,
617}
618
619struct CacheEntry {
620 key: SnapshotCacheKey,
621 blob: Arc<Vec<u8>>,
625}
626
627struct InFlightEntry {
630 result: Mutex<Option<Result<Arc<Vec<u8>>, String>>>,
631 done: Condvar,
632}
633
634impl SnapshotCache {
635 pub fn new(max_entries: usize) -> Self {
636 SnapshotCache {
637 inner: Mutex::new(CacheInner {
638 entries: Vec::new(),
639 in_flight: HashMap::new(),
640 }),
641 max_entries,
642 }
643 }
644
645 pub fn get_or_create(&self, bridge_code: &str) -> Result<Arc<Vec<u8>>, String> {
651 self.get_or_create_with_userland(bridge_code, None)
652 }
653
654 pub fn try_get_with_userland(
657 &self,
658 bridge_code: &str,
659 userland_code: Option<&str>,
660 ) -> Option<Arc<Vec<u8>>> {
661 let key = snapshot_cache_key(bridge_code, userland_code);
662 let mut inner = self.inner.lock().unwrap();
663 if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
664 let entry = inner.entries.remove(pos);
665 let blob = Arc::clone(&entry.blob);
666 inner.entries.push(entry);
667 Some(blob)
668 } else {
669 None
670 }
671 }
672
673 pub fn get_or_create_with_userland(
678 &self,
679 bridge_code: &str,
680 userland_code: Option<&str>,
681 ) -> Result<Arc<Vec<u8>>, String> {
682 let key = snapshot_cache_key(bridge_code, userland_code);
683
684 let in_flight = {
686 let mut inner = self.inner.lock().unwrap();
687
688 if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
690 let entry = inner.entries.remove(pos);
691 let blob = Arc::clone(&entry.blob);
692 inner.entries.push(entry);
693 return Ok(blob);
694 }
695
696 if let Some(entry) = inner.in_flight.get(&key) {
698 Some(Arc::clone(entry))
699 } else {
700 let entry = Arc::new(InFlightEntry {
702 result: Mutex::new(None),
703 done: Condvar::new(),
704 });
705 inner.in_flight.insert(key, Arc::clone(&entry));
706 None
707 }
708 };
709
710 if let Some(entry) = in_flight {
712 let mut result = entry.result.lock().unwrap();
713 while result.is_none() {
714 result = entry.done.wait(result).unwrap();
715 }
716 return result.as_ref().unwrap().clone();
717 }
718
719 let creation_result = create_snapshot_inner(bridge_code, userland_code).map(Arc::new);
721
722 {
724 let mut inner = self.inner.lock().unwrap();
725
726 if let Ok(ref arc) = creation_result {
727 if inner.entries.len() >= self.max_entries {
729 inner.entries.remove(0);
730 }
731 inner.entries.push(CacheEntry {
732 key,
733 blob: Arc::clone(arc),
734 });
735 }
736
737 if let Some(entry) = inner.in_flight.remove(&key) {
739 let mut result = entry.result.lock().unwrap();
740 *result = Some(creation_result.clone());
741 entry.done.notify_all();
742 }
743 }
744
745 creation_result
746 }
747}
748
749pub fn snapshot_cache_key(bridge_code: &str, userland_code: Option<&str>) -> SnapshotCacheKey {
753 match userland_code {
754 None => {
755 let mut hasher = Sha256::new();
756 hasher.update(bridge_code.as_bytes());
757 hasher.finalize().into()
758 }
759 Some(userland_code) => {
760 let mut buf = Vec::with_capacity(bridge_code.len() + 1 + userland_code.len());
761 buf.extend_from_slice(bridge_code.as_bytes());
762 buf.push(0);
763 buf.extend_from_slice(userland_code.as_bytes());
764 let mut hasher = Sha256::new();
765 hasher.update(&buf);
766 hasher.finalize().into()
767 }
768 }
769}
770
771#[doc(hidden)]
772#[cfg(any(test, feature = "test-support"))]
773pub fn run_snapshot_consolidated_checks() {
774 fn eval(isolate: &mut v8::OwnedIsolate, code: &str) -> String {
775 let scope = &mut v8::HandleScope::new(isolate);
776 let context = v8::Context::new(scope, Default::default());
777 let scope = &mut v8::ContextScope::new(scope, context);
778 let source = v8::String::new(scope, code).unwrap();
779 let script = v8::Script::compile(scope, source, None).unwrap();
780 let result = script.run(scope).unwrap();
781 result.to_rust_string_lossy(scope)
782 }
783
784 init_v8_platform();
788 let _ = external_refs();
789
790 {
792 let bridge_code = "(function() { globalThis.__bridge_init = true; })();";
793 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
794 assert!(!blob.is_empty(), "snapshot blob should be non-empty");
795 }
796
797 {
799 let bridge_code = "(function() { globalThis.__testValue = 42; })();";
800 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
801 let mut isolate = create_isolate_from_snapshot(blob, None);
802 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
805 }
806
807 {
809 let bridge_code = "/* empty bridge */";
810 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
811 let mut isolate = create_isolate_from_snapshot(blob, Some(8));
812 assert_eq!(eval(&mut isolate, "'heap ok'"), "heap ok");
813 }
814
815 {
817 let bridge_code = "(function() { globalThis.x = 1; })();";
818 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
819 assert!(
820 blob.len() < MAX_SNAPSHOT_BLOB_BYTES,
821 "normal bridge code should produce blob under 50MB limit"
822 );
823 }
824
825 {
827 let bridge_code = "(function() { globalThis.__counter = 0; })();";
828 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
829 let blob_bytes: Vec<u8> = blob.to_vec();
830
831 for i in 0..3 {
832 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
833 let result = eval(&mut isolate, &format!("{} + 1", i));
834 assert_eq!(result, format!("{}", i + 1));
835 }
836 }
837
838 {
840 let cache = SnapshotCache::new(4);
841 let bridge_code = "(function() { globalThis.__cached = 1; })();";
842
843 let arc1 = cache
844 .get_or_create(bridge_code)
845 .expect("first get_or_create");
846 let arc2 = cache
847 .get_or_create(bridge_code)
848 .expect("second get_or_create");
849
850 assert!(
852 Arc::ptr_eq(&arc1, &arc2),
853 "cache hit should return same Arc"
854 );
855 }
856
857 {
859 let cache = SnapshotCache::new(4);
860 let code_a = "(function() { globalThis.__a = 1; })();";
861 let code_b = "(function() { globalThis.__b = 2; })();";
862
863 let arc_a = cache.get_or_create(code_a).expect("create A");
864 let arc_b = cache.get_or_create(code_b).expect("create B");
865
866 assert!(
868 !Arc::ptr_eq(&arc_a, &arc_b),
869 "different code should produce different Arc"
870 );
871
872 let mut iso_a = create_isolate_from_snapshot((*arc_a).clone(), None);
874 assert_eq!(eval(&mut iso_a, "1 + 1"), "2");
875
876 let mut iso_b = create_isolate_from_snapshot((*arc_b).clone(), None);
877 assert_eq!(eval(&mut iso_b, "2 + 2"), "4");
878 }
879
880 {
882 let cache = SnapshotCache::new(2);
883 let code_1 = "(function() { globalThis.__v1 = 1; })();";
884 let code_2 = "(function() { globalThis.__v2 = 2; })();";
885 let code_3 = "(function() { globalThis.__v3 = 3; })();";
886
887 let arc_1 = cache.get_or_create(code_1).expect("create 1");
888 let _arc_2 = cache.get_or_create(code_2).expect("create 2");
889
890 let _arc_3 = cache.get_or_create(code_3).expect("create 3");
892
893 let arc_1_new = cache.get_or_create(code_1).expect("re-create 1");
895 assert!(
896 !Arc::ptr_eq(&arc_1, &arc_1_new),
897 "evicted entry should produce a new Arc on re-creation"
898 );
899
900 }
905
906 {
908 use std::sync::atomic::{AtomicUsize, Ordering};
909
910 let cache = Arc::new(SnapshotCache::new(4));
911 let bridge_code = "(function() { globalThis.__concurrent = 1; })();";
912
913 let first = cache.get_or_create(bridge_code).expect("pre-warm");
916
917 let num_threads = 4;
918 let barrier = Arc::new(std::sync::Barrier::new(num_threads));
919 let same_count = Arc::new(AtomicUsize::new(0));
920
921 let mut handles = vec![];
922 for _ in 0..num_threads {
923 let cache = Arc::clone(&cache);
924 let barrier = Arc::clone(&barrier);
925 let first = Arc::clone(&first);
926 let same_count = Arc::clone(&same_count);
927 let code = bridge_code.to_string();
928
929 handles.push(std::thread::spawn(move || {
930 barrier.wait();
931 let arc = cache.get_or_create(&code).expect("concurrent get");
932 if Arc::ptr_eq(&arc, &first) {
933 same_count.fetch_add(1, Ordering::Relaxed);
934 }
935 }));
936 }
937
938 for h in handles {
939 h.join().expect("thread join");
940 }
941
942 assert_eq!(
943 same_count.load(Ordering::Relaxed),
944 num_threads,
945 "all concurrent callers should get the same cached Arc"
946 );
947 }
948
949 {
951 let bridge_code = "(function() { globalThis.__wasm_test = true; })();";
952 let blob = create_snapshot(bridge_code).expect("snapshot creation");
953 let mut isolate = create_isolate_from_snapshot(blob, None);
954
955 let scope = &mut v8::HandleScope::new(&mut isolate);
956 let context = v8::Context::new(scope, Default::default());
957 let scope = &mut v8::ContextScope::new(scope, context);
958
959 let wasm_test_code = r#"
960 (function() {
961 var bytes = new Uint8Array([
962 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
963 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
964 0x03, 0x02, 0x01, 0x00,
965 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
966 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
967 ]);
968 var module = new WebAssembly.Module(bytes);
969 var instance = new WebAssembly.Instance(module, {});
970 return String(instance.exports.add(2, 3));
971 })()
972 "#;
973 let source = v8::String::new(scope, wasm_test_code).unwrap();
974 let script = v8::Script::compile(scope, source, None).unwrap();
975 let result = script.run(scope).unwrap();
976 let result_str = result.to_rust_string_lossy(scope);
977
978 assert_eq!(
979 result_str, "5",
980 "WASM should remain enabled after snapshot restore"
981 );
982 }
983
984 {
988 let bridge_code = "(function() { globalThis.__shared_bridge = 'ok'; })();";
989 let blob = create_snapshot(bridge_code).expect("snapshot creation");
990 let blob_bytes: Vec<u8> = blob.to_vec();
991
992 {
994 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
995 let scope = &mut v8::HandleScope::new(&mut isolate);
996 let context = v8::Context::new(scope, Default::default());
997 let scope = &mut v8::ContextScope::new(scope, context);
998
999 let source =
1000 v8::String::new(scope, "globalThis.__session_secret = 'session-a-data';").unwrap();
1001 let script = v8::Script::compile(scope, source, None).unwrap();
1002 script.run(scope);
1003
1004 let check = v8::String::new(scope, "globalThis.__session_secret").unwrap();
1006 let script = v8::Script::compile(scope, check, None).unwrap();
1007 let result = script.run(scope).unwrap();
1008 assert_eq!(result.to_rust_string_lossy(scope), "session-a-data");
1009 }
1010
1011 {
1013 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1014 let scope = &mut v8::HandleScope::new(&mut isolate);
1015 let context = v8::Context::new(scope, Default::default());
1016 let scope = &mut v8::ContextScope::new(scope, context);
1017
1018 let source = v8::String::new(scope, "typeof globalThis.__session_secret").unwrap();
1019 let script = v8::Script::compile(scope, source, None).unwrap();
1020 let result = script.run(scope).unwrap();
1021 assert_eq!(
1022 result.to_rust_string_lossy(scope),
1023 "undefined",
1024 "session B should not see session A's global state"
1025 );
1026 }
1027 }
1028
1029 {
1033 use crate::bridge::{register_async_bridge_fns, register_sync_bridge_fns, PendingPromises};
1034 use crate::host_call::BridgeCallContext;
1035
1036 let bridge_code = "(function() { globalThis.__ext_ref_test = true; })();";
1037 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1038 let mut isolate = create_isolate_from_snapshot(blob, None);
1039
1040 let (event_tx, _event_rx) =
1043 crossbeam_channel::bounded::<crate::session::RuntimeEventEnvelope>(16);
1044 let (_cmd_tx, _cmd_rx) = crossbeam_channel::bounded::<crate::session::SessionCommand>(1);
1045 let call_id_router: crate::host_call::CallIdRouter =
1046 Arc::new(crate::host_call::BridgeCallRegistry::with_default_limit());
1047
1048 let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1049 std::io::Cursor::new(Vec::<u8>::new()),
1050 ));
1051 let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1052 let bridge_ctx = BridgeCallContext::with_receiver(
1053 Box::new(sender),
1054 Box::new(receiver),
1055 "test-session".to_string(),
1056 call_id_router,
1057 Arc::new(std::sync::atomic::AtomicU64::new(1)),
1058 );
1059 let pending = PendingPromises::new();
1060
1061 let scope = &mut v8::HandleScope::new(&mut isolate);
1062 let context = v8::Context::new(scope, Default::default());
1063 let scope = &mut v8::ContextScope::new(scope, context);
1064
1065 let _sync_store = register_sync_bridge_fns(
1067 scope,
1068 &bridge_ctx as *const BridgeCallContext,
1069 &["_testSync"],
1070 );
1071 let _async_store = register_async_bridge_fns(
1072 scope,
1073 &bridge_ctx as *const BridgeCallContext,
1074 &pending as *const PendingPromises,
1075 &["_testAsync"],
1076 );
1077
1078 let check = v8::String::new(scope, "typeof _testSync").unwrap();
1080 let script = v8::Script::compile(scope, check, None).unwrap();
1081 let result = script.run(scope).unwrap();
1082 assert_eq!(
1083 result.to_rust_string_lossy(scope),
1084 "function",
1085 "_testSync should be a function on restored isolate"
1086 );
1087
1088 let check = v8::String::new(scope, "typeof _testAsync").unwrap();
1089 let script = v8::Script::compile(scope, check, None).unwrap();
1090 let result = script.run(scope).unwrap();
1091 assert_eq!(
1092 result.to_rust_string_lossy(scope),
1093 "function",
1094 "_testAsync should be a function on restored isolate"
1095 );
1096 }
1097
1098 {
1102 use crate::bridge::register_stub_bridge_fns;
1103
1104 let bridge_code = "/* stub test */";
1106 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1107 let mut isolate = create_isolate_from_snapshot(blob, None);
1108
1109 let scope = &mut v8::HandleScope::new(&mut isolate);
1110 let context = v8::Context::new(scope, Default::default());
1111 let scope = &mut v8::ContextScope::new(scope, context);
1112
1113 register_stub_bridge_fns(
1114 scope,
1115 &["_log", "_error", "_fsReadFile", "_loadPolyfill"],
1116 &["_scheduleTimer", "_dynamicImport"],
1117 );
1118
1119 let check = v8::String::new(
1120 scope,
1121 r#"
1122 (function() {
1123 var names = ['_log', '_error', '_fsReadFile', '_loadPolyfill',
1124 '_scheduleTimer', '_dynamicImport'];
1125 for (var i = 0; i < names.length; i++) {
1126 if (typeof globalThis[names[i]] !== 'function') {
1127 return 'FAIL: ' + names[i] + ' is ' + typeof globalThis[names[i]];
1128 }
1129 }
1130 return 'OK';
1131 })()
1132 "#,
1133 )
1134 .unwrap();
1135 let script = v8::Script::compile(scope, check, None).unwrap();
1136 let result = script.run(scope).unwrap();
1137 assert_eq!(
1138 result.to_rust_string_lossy(scope),
1139 "OK",
1140 "all stub bridge functions should be registered as functions"
1141 );
1142 }
1143
1144 {
1149 use crate::bridge::register_stub_bridge_fns;
1150
1151 let mut snapshot_isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
1152 {
1153 let scope = &mut v8::HandleScope::new(&mut snapshot_isolate);
1154 let context = v8::Context::new(scope, Default::default());
1155 let scope = &mut v8::ContextScope::new(scope, context);
1156
1157 let sync_bridge_fns = sync_bridge_fns();
1159 let async_bridge_fns = async_bridge_fns();
1160 register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
1161
1162 let iife_code = r#"
1165 (function() {
1166 // Verify bridge functions exist (like ivm-compat shim)
1167 var syncKeys = ['_log', '_error', '_resolveModule', '_loadFile', '_moduleFormat',
1168 '_cryptoRandomFill', '_fsReadFile', '_fsWriteFile',
1169 '_childProcessSpawnStart', '_childProcessPoll', '_childProcessSpawnSync'];
1170 var asyncKeys = ['_dynamicImport', '_scheduleTimer',
1171 '_networkHttpServerListenRaw'];
1172
1173 for (var i = 0; i < syncKeys.length; i++) {
1174 if (typeof globalThis[syncKeys[i]] !== 'function') {
1175 throw new Error('Missing sync: ' + syncKeys[i]);
1176 }
1177 }
1178 for (var i = 0; i < asyncKeys.length; i++) {
1179 if (typeof globalThis[asyncKeys[i]] !== 'function') {
1180 throw new Error('Missing async: ' + asyncKeys[i]);
1181 }
1182 }
1183
1184 // Simulate getter-based fs facade (setup only, no calls)
1185 var _fs = {};
1186 Object.defineProperties(_fs, {
1187 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1188 writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1189 });
1190 globalThis._fs = _fs;
1191
1192 // Verify getter returns function reference without calling it
1193 if (typeof _fs.readFile !== 'function') {
1194 throw new Error('Getter should return function, got ' + typeof _fs.readFile);
1195 }
1196
1197 // Simulate closure wrapping (setup only, no calls)
1198 globalThis.__wrappedLog = function() {
1199 return globalThis._log.apply(null, arguments);
1200 };
1201
1202 globalThis.__bridge_setup_complete = true;
1203 })();
1204 "#;
1205 let source = v8::String::new(scope, iife_code).unwrap();
1206 let script = v8::Script::compile(scope, source, None).unwrap();
1207 let result = script.run(scope);
1208 assert!(
1209 result.is_some(),
1210 "bridge IIFE should execute without error against stub functions"
1211 );
1212
1213 let check =
1215 v8::String::new(scope, "String(globalThis.__bridge_setup_complete)").unwrap();
1216 let script = v8::Script::compile(scope, check, None).unwrap();
1217 let val = script.run(scope).unwrap();
1218 assert_eq!(
1219 val.to_rust_string_lossy(scope),
1220 "true",
1221 "bridge setup should complete with stub functions"
1222 );
1223
1224 scope.set_default_context(context);
1225 }
1226
1227 let blob = snapshot_isolate.create_blob(v8::FunctionCodeHandling::Keep);
1228 assert!(
1229 blob.is_some(),
1230 "snapshot creation should succeed with stub bridge functions"
1231 );
1232 assert!(
1233 !blob.unwrap().is_empty(),
1234 "snapshot blob should be non-empty"
1235 );
1236 }
1237
1238 {
1242 let iife_code = r#"
1244 (function() {
1245 // Verify all sync bridge functions are registered as stubs
1246 var syncFns = ['_log', '_error', '_resolveModule', '_loadFile',
1247 '_moduleFormat', '_loadPolyfill', '_cryptoRandomFill', '_cryptoRandomUUID',
1248 '_fsReadFile', '_fsWriteFile', '_fsReadFileBinary',
1249 '_fsWriteFileBinary', '_fsReadDir', '_fsMkdir', '_fsRmdir',
1250 '_fsExists', '_fsStat', '_fsUnlink', '_fsRename', '_fsChmod',
1251 '_fsChown', '_fsLink', '_fsSymlink', '_fsReadlink', '_fsLstat',
1252 '_fsTruncate', '_fsUtimes', '_childProcessSpawnStart',
1253 '_childProcessPoll', '_childProcessStdinWrite', '_childProcessStdinClose',
1254 '_childProcessKill', '_childProcessSpawnSync'];
1255 for (var i = 0; i < syncFns.length; i++) {
1256 if (typeof globalThis[syncFns[i]] !== 'function') {
1257 throw new Error('Missing sync stub: ' + syncFns[i] +
1258 ' (typeof=' + typeof globalThis[syncFns[i]] + ')');
1259 }
1260 }
1261
1262 // Verify all async bridge functions are registered as stubs
1263 var asyncFns = ['_dynamicImport', '_scheduleTimer',
1264 '_networkDnsLookupRaw',
1265 '_networkDnsResolveRaw',
1266 '_networkHttpServerListenRaw',
1267 '_networkHttpServerCloseRaw', '_networkHttpServerWaitRaw',
1268 '_networkHttp2ServerWaitRaw', '_networkHttp2SessionWaitRaw'];
1269 for (var i = 0; i < asyncFns.length; i++) {
1270 if (typeof globalThis[asyncFns[i]] !== 'function') {
1271 throw new Error('Missing async stub: ' + asyncFns[i] +
1272 ' (typeof=' + typeof globalThis[asyncFns[i]] + ')');
1273 }
1274 }
1275
1276 // Verify _processConfig default was injected
1277 if (typeof _processConfig !== 'object' || _processConfig === null) {
1278 throw new Error('_processConfig not injected: ' + typeof _processConfig);
1279 }
1280 if (_processConfig.cwd !== '/') {
1281 throw new Error('_processConfig.cwd should be "/", got: ' + _processConfig.cwd);
1282 }
1283
1284 // Verify _osConfig default was injected
1285 if (typeof _osConfig !== 'object' || _osConfig === null) {
1286 throw new Error('_osConfig not injected: ' + typeof _osConfig);
1287 }
1288 if (_osConfig.platform !== 'linux') {
1289 throw new Error('_osConfig.platform should be "linux", got: ' + _osConfig.platform);
1290 }
1291
1292 globalThis.__part15_ok = true;
1293 })();
1294 "#;
1295 let blob = create_snapshot(iife_code).expect(
1296 "create_snapshot should succeed with bridge code that checks stubs and defaults",
1297 );
1298 assert!(!blob.is_empty(), "snapshot blob should be non-empty");
1299
1300 let mut isolate = create_isolate_from_snapshot(blob, None);
1302 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1303 }
1304
1305 {
1310 let iife_code = r#"
1311 (function() {
1312 // Set up getter-based fs facade referencing bridge stubs
1313 var _fs = {};
1314 Object.defineProperties(_fs, {
1315 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1316 writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1317 });
1318 globalThis._fs = _fs;
1319
1320 // Set up closure wrapping a bridge stub
1321 globalThis.myLog = function() {
1322 return globalThis._log.apply(null, arguments);
1323 };
1324
1325 // Set up a require-like function (doesn't call _loadPolyfill at setup)
1326 globalThis.require = function(name) {
1327 return globalThis._loadPolyfill(name);
1328 };
1329
1330 // Set up a console-like object
1331 globalThis.console = {
1332 log: function() { globalThis._log.apply(null, arguments); },
1333 error: function() { globalThis._error.apply(null, arguments); },
1334 };
1335
1336 // Read _processConfig at setup time (like process.cwd initialization)
1337 globalThis.__initialCwd = _processConfig.cwd;
1338
1339 globalThis.__part16_setup = true;
1340 })();
1341 "#;
1342 let blob = create_snapshot(iife_code)
1343 .expect("create_snapshot should succeed with full bridge IIFE pattern");
1344 assert!(!blob.is_empty());
1345
1346 let blob_bytes: Vec<u8> = blob.to_vec();
1348 let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1349 let scope = &mut v8::HandleScope::new(&mut isolate);
1350 let context = v8::Context::new(scope, Default::default());
1351 let scope = &mut v8::ContextScope::new(scope, context);
1352
1353 let check_code = r#"
1355 (function() {
1356 var results = [];
1357 results.push('_fs=' + (typeof _fs === 'object'));
1358 results.push('_fs.readFile=' + (typeof _fs.readFile === 'function'));
1359 results.push('myLog=' + (typeof myLog === 'function'));
1360 results.push('require=' + (typeof require === 'function'));
1361 results.push('console.log=' + (typeof console.log === 'function'));
1362 results.push('console.error=' + (typeof console.error === 'function'));
1363 results.push('__initialCwd=' + __initialCwd);
1364 results.push('__part16_setup=' + __part16_setup);
1365 return results.join(';');
1366 })()
1367 "#;
1368 let source = v8::String::new(scope, check_code).unwrap();
1369 let script = v8::Script::compile(scope, source, None).unwrap();
1370 let result = script.run(scope).unwrap();
1371 let result_str = result.to_rust_string_lossy(scope);
1372
1373 assert_eq!(
1374 result_str,
1375 "_fs=true;_fs.readFile=true;myLog=true;require=true;console.log=true;console.error=true;__initialCwd=/;__part16_setup=true",
1376 "restored context should have all bridge infrastructure from the IIFE"
1377 );
1378 }
1379
1380 {
1383 let cache = SnapshotCache::new(4);
1384 let code = r#"
1385 (function() {
1386 // Verify stubs are present (create_snapshot registers them)
1387 if (typeof _log !== 'function') throw new Error('no _log stub');
1388 if (typeof _processConfig !== 'object') throw new Error('no _processConfig');
1389 globalThis.__cached_context = true;
1390 })();
1391 "#;
1392
1393 let arc1 = cache.get_or_create(code).expect("first get_or_create");
1394 let arc2 = cache.get_or_create(code).expect("second get_or_create");
1395 assert!(
1396 Arc::ptr_eq(&arc1, &arc2),
1397 "cache hit should return same Arc"
1398 );
1399
1400 let mut isolate = create_isolate_from_snapshot((*arc1).clone(), None);
1402 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1403 }
1404
1405 {
1410 use crate::bridge::{replace_bridge_fns, PendingPromises};
1411 use crate::host_call::BridgeCallContext;
1412
1413 let bridge_code = r#"
1415 (function() {
1416 // Getter-based facade referencing globalThis._fsReadFile
1417 var _fs = {};
1418 Object.defineProperties(_fs, {
1419 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1420 });
1421 globalThis._fs = _fs;
1422 globalThis.__bridge_ready = true;
1423 })();
1424 "#;
1425 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1426 let mut isolate = create_isolate_from_snapshot(blob, None);
1427
1428 let (event_tx, _event_rx) =
1430 crossbeam_channel::bounded::<crate::session::RuntimeEventEnvelope>(16);
1431 let call_id_router: crate::host_call::CallIdRouter =
1432 Arc::new(crate::host_call::BridgeCallRegistry::with_default_limit());
1433 let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1434 std::io::Cursor::new(Vec::<u8>::new()),
1435 ));
1436 let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1437 let bridge_ctx = BridgeCallContext::with_receiver(
1438 Box::new(sender),
1439 Box::new(receiver),
1440 "test-session".to_string(),
1441 call_id_router,
1442 Arc::new(std::sync::atomic::AtomicU64::new(1)),
1443 );
1444 let pending = PendingPromises::new();
1445
1446 let scope = &mut v8::HandleScope::new(&mut isolate);
1448 let context = v8::Context::new(scope, Default::default());
1449 let scope = &mut v8::ContextScope::new(scope, context);
1450
1451 let (_sync_store, _async_store) = replace_bridge_fns(
1452 scope,
1453 &bridge_ctx as *const BridgeCallContext,
1454 &pending as *const PendingPromises,
1455 &["_log", "_fsReadFile"],
1456 &["_scheduleTimer"],
1457 );
1458
1459 let check = v8::String::new(
1461 scope,
1462 r#"
1463 (function() {
1464 var results = [];
1465 results.push('__bridge_ready=' + globalThis.__bridge_ready);
1466 results.push('_fs_exists=' + (typeof _fs === 'object'));
1467 // Getter should resolve to the REPLACED function (not stub)
1468 results.push('_fs.readFile_type=' + typeof _fs.readFile);
1469 // Direct global should also be the replaced function
1470 results.push('_log_type=' + typeof _log);
1471 results.push('_scheduleTimer_type=' + typeof _scheduleTimer);
1472 return results.join(';');
1473 })()
1474 "#,
1475 )
1476 .unwrap();
1477 let script = v8::Script::compile(scope, check, None).unwrap();
1478 let result = script.run(scope).unwrap();
1479 assert_eq!(
1480 result.to_rust_string_lossy(scope),
1481 "__bridge_ready=true;_fs_exists=true;_fs.readFile_type=function;_log_type=function;_scheduleTimer_type=function",
1482 "restored context should have bridge IIFE state + replaced functions"
1483 );
1484 }
1485
1486 {
1490 use crate::bridge::serialize_v8_value;
1491
1492 let bridge_code = r#"
1493 (function() {
1494 // Verify default _processConfig from snapshot
1495 globalThis.__snapshotCwd = _processConfig.cwd;
1496 })();
1497 "#;
1498 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1499 let mut isolate = create_isolate_from_snapshot(blob, None);
1500
1501 let scope = &mut v8::HandleScope::new(&mut isolate);
1502 let context = v8::Context::new(scope, Default::default());
1503 let scope = &mut v8::ContextScope::new(scope, context);
1504
1505 let check = v8::String::new(scope, "__snapshotCwd").unwrap();
1507 let script = v8::Script::compile(scope, check, None).unwrap();
1508 let result = script.run(scope).unwrap();
1509 assert_eq!(result.to_rust_string_lossy(scope), "/");
1510
1511 let payload_code = r#"({
1513 processConfig: { cwd: "/app", env: { FOO: "bar" }, timing_mitigation: "off", frozen_time_ms: null },
1514 osConfig: { homedir: "/home/agentos", tmpdir: "/tmp", platform: "linux", arch: "arm64" }
1515 })"#;
1516 let payload_source = v8::String::new(scope, payload_code).unwrap();
1517 let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
1518 let payload_val = payload_script.run(scope).unwrap();
1519 let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
1520
1521 crate::execution::inject_globals_from_payload(scope, &payload_bytes)
1523 .expect("inject globals payload");
1524
1525 let check = v8::String::new(scope, "_processConfig.cwd").unwrap();
1527 let script = v8::Script::compile(scope, check, None).unwrap();
1528 let result = script.run(scope).unwrap();
1529 assert_eq!(
1530 result.to_rust_string_lossy(scope),
1531 "/app",
1532 "_processConfig.cwd should be overridden from '/' to '/app'"
1533 );
1534
1535 let check = v8::String::new(scope, "_osConfig.arch").unwrap();
1537 let script = v8::Script::compile(scope, check, None).unwrap();
1538 let result = script.run(scope).unwrap();
1539 assert_eq!(
1540 result.to_rust_string_lossy(scope),
1541 "arm64",
1542 "_osConfig.arch should be overridden to 'arm64'"
1543 );
1544 }
1545
1546 {
1548 let bridge_code = r#"
1549 (function() {
1550 globalThis.__snapshotFn = async function () { return "ok"; };
1551 })();
1552 "#;
1553 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1554 let mut isolate = create_isolate_from_snapshot(blob, None);
1555
1556 let scope = &mut v8::HandleScope::new(&mut isolate);
1557 let context = v8::Context::new(scope, Default::default());
1558 let scope = &mut v8::ContextScope::new(scope, context);
1559
1560 let check = v8::String::new(
1561 scope,
1562 r#"(function() {
1563 return JSON.stringify({
1564 fnType: typeof globalThis.__snapshotFn,
1565 promiseType: typeof globalThis.__snapshotFn?.(),
1566 });
1567 })()"#,
1568 )
1569 .unwrap();
1570 let script = v8::Script::compile(scope, check, None).unwrap();
1571 let result = script.run(scope).unwrap();
1572 assert_eq!(
1573 result.to_rust_string_lossy(scope),
1574 r#"{"fnType":"function","promiseType":"object"}"#,
1575 "function-valued globals should survive snapshot restore"
1576 );
1577 }
1578
1579 {
1581 let bridge_code = concat!(
1582 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1583 "\n",
1584 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1585 );
1586 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1587 let mut isolate = create_isolate_from_snapshot(blob, None);
1588
1589 let scope = &mut v8::HandleScope::new(&mut isolate);
1590 let context = v8::Context::new(scope, Default::default());
1591 let scope = &mut v8::ContextScope::new(scope, context);
1592
1593 let check = v8::String::new(
1594 scope,
1595 r#"(function() {
1596 return JSON.stringify({
1597 fetchType: typeof globalThis.fetch,
1598 headersType: typeof globalThis.Headers,
1599 requestType: typeof globalThis.Request,
1600 responseType: typeof globalThis.Response,
1601 });
1602 })()"#,
1603 )
1604 .unwrap();
1605 let script = v8::Script::compile(scope, check, None).unwrap();
1606 let result = script.run(scope).unwrap();
1607 assert_eq!(
1608 result.to_rust_string_lossy(scope),
1609 r#"{"fetchType":"function","headersType":"function","requestType":"function","responseType":"function"}"#,
1610 "bundled bridge should expose fetch globals in restored contexts"
1611 );
1612 }
1613
1614 {
1618 use std::sync::atomic::{AtomicBool, Ordering};
1619 use std::time::Instant;
1620
1621 let cache = Arc::new(SnapshotCache::new(4));
1622 let codes: Vec<String> = (0..3)
1623 .map(|i| {
1624 format!(
1625 "(function() {{ globalThis.__concurrent_{} = {}; }})();",
1626 i, i
1627 )
1628 })
1629 .collect();
1630
1631 let barrier = Arc::new(std::sync::Barrier::new(codes.len()));
1632 let all_ok = Arc::new(AtomicBool::new(true));
1633
1634 let mut handles = vec![];
1635 for code in &codes {
1636 let cache = Arc::clone(&cache);
1637 let barrier = Arc::clone(&barrier);
1638 let all_ok = Arc::clone(&all_ok);
1639 let code = code.clone();
1640
1641 handles.push(std::thread::spawn(move || {
1642 barrier.wait();
1643 let start = Instant::now();
1644 match cache.get_or_create(&code) {
1645 Ok(arc) => {
1646 assert!(!arc.is_empty());
1647 }
1648 Err(e) => {
1649 eprintln!("get_or_create failed: {}", e);
1650 all_ok.store(false, Ordering::Relaxed);
1651 }
1652 }
1653 start.elapsed()
1654 }));
1655 }
1656
1657 let mut durations = vec![];
1658 for h in handles {
1659 durations.push(h.join().expect("thread join"));
1660 }
1661
1662 assert!(
1663 all_ok.load(Ordering::Relaxed),
1664 "all concurrent get_or_create calls should succeed"
1665 );
1666
1667 for code in &codes {
1669 let arc1 = cache.get_or_create(code).unwrap();
1670 let arc2 = cache.get_or_create(code).unwrap();
1671 assert!(
1672 Arc::ptr_eq(&arc1, &arc2),
1673 "should be cache hit after creation"
1674 );
1675 }
1676 }
1677
1678 {
1681 let bridge_code = r#"
1682 (function() {
1683 globalThis.__bridge_ok = true;
1684 })();
1685 "#;
1686 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1687 let blob_bytes: Vec<u8> = blob.to_vec();
1688
1689 {
1691 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1692 let scope = &mut v8::HandleScope::new(&mut isolate);
1693 let context = v8::Context::new(scope, Default::default());
1694 let scope = &mut v8::ContextScope::new(scope, context);
1695
1696 let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1698 let script = v8::Script::compile(scope, check, None).unwrap();
1699 let result = script.run(scope).unwrap();
1700 assert_eq!(result.to_rust_string_lossy(scope), "true");
1701
1702 let code = v8::String::new(scope, "globalThis.__user_data = 'session-a';").unwrap();
1704 let script = v8::Script::compile(scope, code, None).unwrap();
1705 script.run(scope);
1706 }
1707
1708 {
1710 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1711 let scope = &mut v8::HandleScope::new(&mut isolate);
1712 let context = v8::Context::new(scope, Default::default());
1713 let scope = &mut v8::ContextScope::new(scope, context);
1714
1715 let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1717 let script = v8::Script::compile(scope, check, None).unwrap();
1718 let result = script.run(scope).unwrap();
1719 assert_eq!(result.to_rust_string_lossy(scope), "true");
1720
1721 let check = v8::String::new(scope, "typeof __user_data").unwrap();
1723 let script = v8::Script::compile(scope, check, None).unwrap();
1724 let result = script.run(scope).unwrap();
1725 assert_eq!(
1726 result.to_rust_string_lossy(scope),
1727 "undefined",
1728 "session B should not see session A's user data"
1729 );
1730 }
1731 }
1732
1733 {
1736 fn run_in(scope: &mut v8::ContextScope<v8::HandleScope>, code: &str) -> String {
1740 let source = v8::String::new(scope, code).unwrap();
1741 let script = v8::Script::compile(scope, source, None).unwrap();
1742 let result = script.run(scope).unwrap();
1743 result.to_rust_string_lossy(scope)
1744 }
1745
1746 let bridge_code = "(function(){ globalThis.__bridge_ok = true; })();";
1751 let userland = r#"
1752 (function () {
1753 if (typeof globalThis._fsReadFile !== "function") {
1754 throw new Error("bridge fns missing during userland eval");
1755 }
1756 globalThis.__sideEffectCount = (globalThis.__sideEffectCount || 0) + 1;
1757 var secret = 42;
1758 globalThis.__x = { f: function () { return secret; } };
1759 })();
1760 "#;
1761
1762 let blob = create_snapshot_with_userland(bridge_code, userland)
1763 .expect("userland snapshot creation should succeed");
1764 let blob_bytes: Vec<u8> = blob.to_vec();
1765
1766 {
1769 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1770 let scope = &mut v8::HandleScope::new(&mut isolate);
1771 let context = v8::Context::new(scope, Default::default());
1772 let scope = &mut v8::ContextScope::new(scope, context);
1773
1774 assert_eq!(
1775 run_in(scope, "String(globalThis.__x.f())"),
1776 "42",
1777 "userland export __x.f() should return 42 from the snapshot"
1778 );
1779 assert_eq!(
1780 run_in(scope, "String(globalThis.__sideEffectCount)"),
1781 "1",
1782 "userland top-level must run exactly once (zero re-eval on restore)"
1783 );
1784 assert_eq!(
1785 run_in(scope, "String(globalThis.__bridge_ok)"),
1786 "true",
1787 "bridge state should coexist with userland state in the snapshot"
1788 );
1789
1790 run_in(scope, "globalThis.__leak = 'session-a'; ''");
1792 }
1793
1794 {
1797 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1798 let scope = &mut v8::HandleScope::new(&mut isolate);
1799 let context = v8::Context::new(scope, Default::default());
1800 let scope = &mut v8::ContextScope::new(scope, context);
1801
1802 assert_eq!(
1803 run_in(scope, "String(globalThis.__x.f())"),
1804 "42",
1805 "session B should see the captured userland export"
1806 );
1807 assert_eq!(
1808 run_in(scope, "String(globalThis.__sideEffectCount)"),
1809 "1",
1810 "session B counter must still be 1 (no re-eval, no cross-session bump)"
1811 );
1812 assert_eq!(
1813 run_in(scope, "typeof globalThis.__leak"),
1814 "undefined",
1815 "session B must NOT observe session A's mutation"
1816 );
1817 }
1818
1819 {
1821 let cache = SnapshotCache::new(4);
1822 let a = cache
1823 .get_or_create_with_userland(bridge_code, Some(userland))
1824 .expect("userland cache create");
1825 let b = cache
1826 .get_or_create_with_userland(bridge_code, Some(userland))
1827 .expect("userland cache hit");
1828 assert!(
1829 Arc::ptr_eq(&a, &b),
1830 "identical userland should hit the cache"
1831 );
1832
1833 let userland2 = "(function(){ globalThis.__x = { f: function(){ return 7; } }; })();";
1834 let c = cache
1835 .get_or_create_with_userland(bridge_code, Some(userland2))
1836 .expect("changed userland create");
1837 assert!(
1838 !Arc::ptr_eq(&a, &c),
1839 "changed userland (dep-graph change) should rebuild"
1840 );
1841 }
1842 }
1843
1844 if let Ok(bundle_path) = std::env::var("PI_SNAPSHOT_BUNDLE_PATH") {
1851 let userland = std::fs::read_to_string(&bundle_path)
1852 .unwrap_or_else(|e| panic!("read pi bundle at {bundle_path}: {e}"));
1853 let bridge_code = concat!(
1854 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1855 "\n",
1856 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1857 );
1858
1859 let blob = create_snapshot_with_userland(bridge_code, &userland)
1860 .expect("real pi SDK bundle should snapshot cleanly (pure-JS, no top-level I/O)");
1861 let mut isolate = create_isolate_from_snapshot(blob, None);
1862 let scope = &mut v8::HandleScope::new(&mut isolate);
1863 let context = v8::Context::new(scope, Default::default());
1864 let scope = &mut v8::ContextScope::new(scope, context);
1865
1866 let check = v8::String::new(
1867 scope,
1868 "(function(){ var r = globalThis.__PI_SDK_RUNTIME__; \
1869 return r && typeof r.createAgentSession === 'function' && \
1870 typeof r.createAllTools === 'function' ? 'ok' : 'missing'; })()",
1871 )
1872 .unwrap();
1873 let script = v8::Script::compile(scope, check, None).unwrap();
1874 let result = script.run(scope).unwrap();
1875 assert_eq!(
1876 result.to_rust_string_lossy(scope),
1877 "ok",
1878 "restored isolate must expose the pi SDK runtime global from the snapshot"
1879 );
1880 }
1881
1882 {
1887 let bridge_code = "(function(){ globalThis.__xt_bridge = true; })();";
1888 let userland = "(function(){ globalThis.__xt = { f: function(){ return 99; } }; })();";
1889 let blob_bytes: Vec<u8> = std::thread::spawn(move || {
1890 create_snapshot_with_userland(bridge_code, userland)
1891 .expect("cross-thread snapshot build should succeed")
1892 .to_vec()
1893 })
1894 .join()
1895 .expect("build thread join");
1896
1897 let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1898 let scope = &mut v8::HandleScope::new(&mut isolate);
1899 let context = v8::Context::new(scope, Default::default());
1900 let scope = &mut v8::ContextScope::new(scope, context);
1901 let check = v8::String::new(scope, "String(globalThis.__xt.f())").unwrap();
1902 let script = v8::Script::compile(scope, check, None).unwrap();
1903 let result = script.run(scope).unwrap();
1904 assert_eq!(
1905 result.to_rust_string_lossy(scope),
1906 "99",
1907 "a snapshot built on another thread must restore correctly on this thread"
1908 );
1909 }
1910
1911 {
1916 let bridge_code = "(function(){ globalThis.__iso_ok = true; })();";
1917 let userland = "(function(){ globalThis.__sdk = { v: 1 }; })();";
1918 let blob_bytes: Vec<u8> = create_snapshot_with_userland(bridge_code, userland)
1919 .expect("isolation snapshot")
1920 .to_vec();
1921
1922 {
1924 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1925 let scope = &mut v8::HandleScope::new(&mut isolate);
1926 let context = v8::Context::new(scope, Default::default());
1927 let scope = &mut v8::ContextScope::new(scope, context);
1928 let src = v8::String::new(
1929 scope,
1930 "globalThis.__leakG = 'A'; globalThis.__sdk.v = 999; \
1931 Array.prototype.__leakP = 'A'; ''",
1932 )
1933 .unwrap();
1934 let script = v8::Script::compile(scope, src, None).unwrap();
1935 script.run(scope);
1936 }
1937
1938 {
1941 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1942 let scope = &mut v8::HandleScope::new(&mut isolate);
1943 let context = v8::Context::new(scope, Default::default());
1944 let scope = &mut v8::ContextScope::new(scope, context);
1945 let check = v8::String::new(
1946 scope,
1947 "(function(){ return [ \
1948 String(globalThis.__iso_ok), \
1949 typeof globalThis.__leakG, \
1950 String(globalThis.__sdk.v), \
1951 typeof ([].__leakP) \
1952 ].join(','); })()",
1953 )
1954 .unwrap();
1955 let script = v8::Script::compile(scope, check, None).unwrap();
1956 let result = script.run(scope).unwrap();
1957 assert_eq!(
1958 result.to_rust_string_lossy(scope),
1959 "true,undefined,1,undefined",
1960 "session B must see snapshot state but NOT session A's global/SDK/prototype mutations"
1961 );
1962 }
1963 }
1964
1965 {
1977 use crate::bridge::serialize_v8_value;
1978
1979 let bridge_code = r#"
1985 (function () {
1986 globalThis.process = {
1987 get versions() {
1988 // Throws during creation (default _processConfig has no
1989 // version); returns the live per-session value post-restore.
1990 return { node: _processConfig.version.replace(/^v/, "") };
1991 },
1992 };
1993 })();
1994 "#;
1995 let userland = "(function(){ globalThis.__sdk_ready = true; })();";
1996 let blob = create_snapshot_with_userland(bridge_code, userland)
1997 .expect("userland snapshot with a lazy process.versions getter");
1998
1999 let mut isolate = create_isolate_from_snapshot(blob, None);
2000 let scope = &mut v8::HandleScope::new(&mut isolate);
2001 let context = v8::Context::new(scope, Default::default());
2002 let scope = &mut v8::ContextScope::new(scope, context);
2003
2004 let payload_code = r#"({
2007 processConfig: { cwd: "/", env: {}, version: "v99.1.2", timing_mitigation: "off", frozen_time_ms: null },
2008 osConfig: { homedir: "/root", tmpdir: "/tmp", platform: "linux", arch: "x64" }
2009 })"#;
2010 let payload_source = v8::String::new(scope, payload_code).unwrap();
2011 let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
2012 let payload_val = payload_script.run(scope).unwrap();
2013 let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
2014 crate::execution::inject_globals_from_payload(scope, &payload_bytes)
2015 .expect("inject per-session config");
2016
2017 let check = v8::String::new(scope, "String(process.versions.node)").unwrap();
2020 let script = v8::Script::compile(scope, check, None).unwrap();
2021 let result = script.run(scope).unwrap();
2022 assert_eq!(
2023 result.to_rust_string_lossy(scope),
2024 "99.1.2",
2025 "process.versions must defer to the live per-session getter post-restore, \
2026 not the frozen snapshot-build-time static identity (H-1 regression)"
2027 );
2028 }
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 #[test]
2036 fn bridge_cache_key_uses_full_sha256_digest() {
2037 assert_eq!(
2040 snapshot_cache_key("abc", None),
2041 [
2042 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
2043 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
2044 0xf2, 0x00, 0x15, 0xad,
2045 ]
2046 );
2047 }
2048
2049 #[test]
2050 fn create_snapshot_rejects_oversized_bridge_code_before_v8_creation() {
2051 let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2052 let error = match create_snapshot(&bridge_code) {
2053 Ok(_) => panic!("oversized bridge code should be rejected"),
2054 Err(error) => error,
2055 };
2056
2057 assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2058 assert!(error.contains("bridge code too large for V8 bridge setup"));
2059 assert!(error.contains(&MAX_V8_BRIDGE_CODE_BYTES.to_string()));
2060 }
2061
2062 #[test]
2063 fn snapshot_cache_rejects_oversized_bridge_code_without_retaining_in_flight_state() {
2064 let cache = SnapshotCache::new(1);
2065 let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2066
2067 for _ in 0..2 {
2068 let error = match cache.get_or_create(&bridge_code) {
2069 Ok(_) => panic!("oversized bridge code should be rejected"),
2070 Err(error) => error,
2071 };
2072
2073 assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2074 }
2075 }
2076
2077 #[test]
2078 fn snapshot_cache_key_is_dep_keyed_over_bridge_and_userland() {
2079 let bridge = "bridge-a";
2080 assert_ne!(
2082 snapshot_cache_key(bridge, None),
2083 snapshot_cache_key(bridge, Some("user-1")),
2084 "adding userland must change the key"
2085 );
2086 assert_ne!(
2088 snapshot_cache_key(bridge, Some("user-1")),
2089 snapshot_cache_key(bridge, Some("user-2")),
2090 "different userland must produce a different key"
2091 );
2092 assert_eq!(
2094 snapshot_cache_key(bridge, Some("user-1")),
2095 snapshot_cache_key(bridge, Some("user-1")),
2096 );
2097 assert_ne!(
2099 snapshot_cache_key("ab", Some("c")),
2100 snapshot_cache_key("a", Some("bc")),
2101 "the bridge/userland split must be unambiguous"
2102 );
2103 }
2104
2105 #[test]
2106 fn create_snapshot_with_userland_rejects_oversized_userland_code() {
2107 let bridge_code = "(function(){})();";
2108 let userland = " ".repeat(MAX_V8_USERLAND_CODE_BYTES + 1);
2109 let error = match create_snapshot_with_userland(bridge_code, &userland) {
2110 Ok(_) => panic!("oversized userland code should be rejected"),
2111 Err(error) => error,
2112 };
2113
2114 assert!(error.contains(V8_USERLAND_CODE_LIMIT_ERROR_CODE));
2115 assert!(error.contains("userland snapshot code too large"));
2116 }
2117}