@@ -66,6 +66,10 @@ pub fn create_snapshot(bridge_code: &str) -> Result<v8::StartupData, String> {
6666/// These are placeholder values so bridge code that reads _processConfig or
6767/// _osConfig at setup time doesn't fail. They're overwritten per-session
6868/// after snapshot restore via inject_globals_from_payload.
69+ ///
70+ /// Properties are set as READ_ONLY (not DONT_DELETE) so they remain
71+ /// configurable — inject_globals_from_payload can redefine them with
72+ /// READ_ONLY | DONT_DELETE after restore.
6973fn inject_snapshot_defaults ( scope : & mut v8:: HandleScope ) {
7074 let context = scope. get_current_context ( ) ;
7175 let global = context. global ( scope) ;
@@ -84,7 +88,9 @@ fn inject_snapshot_defaults(scope: &mut v8::HandleScope) {
8488 pc_obj. set_integrity_level ( scope, v8:: IntegrityLevel :: Frozen ) ;
8589 }
8690 let pc_key = v8:: String :: new ( scope, "_processConfig" ) . unwrap ( ) ;
87- let attr = v8:: PropertyAttribute :: READ_ONLY | v8:: PropertyAttribute :: DONT_DELETE ;
91+ // READ_ONLY only — no DONT_DELETE so the property remains configurable
92+ // for override after snapshot restore
93+ let attr = v8:: PropertyAttribute :: READ_ONLY ;
8894 global. define_own_property ( scope, pc_key. into ( ) , pc_val, attr) ;
8995
9096 // _osConfig: default placeholder (overwritten per-session)
@@ -101,7 +107,8 @@ fn inject_snapshot_defaults(scope: &mut v8::HandleScope) {
101107 oc_obj. set_integrity_level ( scope, v8:: IntegrityLevel :: Frozen ) ;
102108 }
103109 let oc_key = v8:: String :: new ( scope, "_osConfig" ) . unwrap ( ) ;
104- let attr2 = v8:: PropertyAttribute :: READ_ONLY | v8:: PropertyAttribute :: DONT_DELETE ;
110+ // READ_ONLY only — no DONT_DELETE so the property remains configurable
111+ let attr2 = v8:: PropertyAttribute :: READ_ONLY ;
105112 global. define_own_property ( scope, oc_key. into ( ) , oc_val, attr2) ;
106113}
107114
@@ -822,5 +829,201 @@ mod tests {
822829 let mut isolate = create_isolate_from_snapshot ( ( * arc1) . clone ( ) , None ) ;
823830 assert_eq ! ( eval( & mut isolate, "1 + 1" ) , "2" ) ;
824831 }
832+
833+ // --- Part 18: Context restore + replace_bridge_fns dispatches correctly ---
834+ // Verifies the full context snapshot restore flow: create snapshot with
835+ // stubs, restore, replace stubs with real bridge functions, verify the
836+ // replaced functions dispatch to the real Rust callbacks.
837+ {
838+ use std:: cell:: RefCell ;
839+ use crate :: bridge:: {
840+ replace_bridge_fns, SessionBuffers , PendingPromises ,
841+ } ;
842+ use crate :: host_call:: BridgeCallContext ;
843+
844+ // Create snapshot with stubs + simple bridge IIFE
845+ let bridge_code = r#"
846+ (function() {
847+ // Getter-based facade referencing globalThis._fsReadFile
848+ var _fs = {};
849+ Object.defineProperties(_fs, {
850+ readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
851+ });
852+ globalThis._fs = _fs;
853+ globalThis.__bridge_ready = true;
854+ })();
855+ "# ;
856+ let blob = create_snapshot ( bridge_code) . expect ( "snapshot creation" ) ;
857+ let mut isolate = create_isolate_from_snapshot ( blob, None ) ;
858+ crate :: execution:: disable_wasm ( & mut isolate) ;
859+
860+ // Create BridgeCallContext (sync calls will fail but we verify dispatch)
861+ let ( ipc_tx, _ipc_rx) = crossbeam_channel:: unbounded :: < Vec < u8 > > ( ) ;
862+ let call_id_router: crate :: host_call:: CallIdRouter =
863+ Arc :: new ( Mutex :: new ( std:: collections:: HashMap :: new ( ) ) ) ;
864+ let receiver = crate :: host_call:: ReaderResponseReceiver :: new (
865+ Box :: new ( std:: io:: Cursor :: new ( Vec :: < u8 > :: new ( ) ) ) ,
866+ ) ;
867+ let sender = crate :: host_call:: ChannelFrameSender :: new ( ipc_tx) ;
868+ let bridge_ctx = BridgeCallContext :: with_receiver (
869+ Box :: new ( sender) ,
870+ Box :: new ( receiver) ,
871+ "test-session" . to_string ( ) ,
872+ call_id_router,
873+ ) ;
874+ let session_buffers = RefCell :: new ( SessionBuffers :: new ( ) ) ;
875+ let pending = PendingPromises :: new ( ) ;
876+
877+ // Restore context and replace bridge functions
878+ let scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
879+ let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
880+ let scope = & mut v8:: ContextScope :: new ( scope, context) ;
881+
882+ let ( _sync_store, _async_store) = replace_bridge_fns (
883+ scope,
884+ & bridge_ctx as * const BridgeCallContext ,
885+ & pending as * const PendingPromises ,
886+ & session_buffers as * const RefCell < SessionBuffers > ,
887+ & [ "_log" , "_fsReadFile" ] ,
888+ & [ "_scheduleTimer" ] ,
889+ ) ;
890+
891+ // Verify bridge infrastructure from IIFE survives restore
892+ let check = v8:: String :: new ( scope, r#"
893+ (function() {
894+ var results = [];
895+ results.push('__bridge_ready=' + globalThis.__bridge_ready);
896+ results.push('_fs_exists=' + (typeof _fs === 'object'));
897+ // Getter should resolve to the REPLACED function (not stub)
898+ results.push('_fs.readFile_type=' + typeof _fs.readFile);
899+ // Direct global should also be the replaced function
900+ results.push('_log_type=' + typeof _log);
901+ results.push('_scheduleTimer_type=' + typeof _scheduleTimer);
902+ return results.join(';');
903+ })()
904+ "# ) . unwrap ( ) ;
905+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
906+ let result = script. run ( scope) . unwrap ( ) ;
907+ assert_eq ! (
908+ result. to_rust_string_lossy( scope) ,
909+ "__bridge_ready=true;_fs_exists=true;_fs.readFile_type=function;_log_type=function;_scheduleTimer_type=function" ,
910+ "restored context should have bridge IIFE state + replaced functions"
911+ ) ;
912+ }
913+
914+ // --- Part 19: _processConfig is overridable after restore ---
915+ // Verifies that inject_snapshot_defaults uses configurable properties
916+ // so inject_globals_from_payload can override them per session.
917+ {
918+ use crate :: bridge:: serialize_v8_value;
919+
920+ let bridge_code = r#"
921+ (function() {
922+ // Verify default _processConfig from snapshot
923+ globalThis.__snapshotCwd = _processConfig.cwd;
924+ })();
925+ "# ;
926+ let blob = create_snapshot ( bridge_code) . expect ( "snapshot creation" ) ;
927+ let mut isolate = create_isolate_from_snapshot ( blob, None ) ;
928+
929+ let scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
930+ let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
931+ let scope = & mut v8:: ContextScope :: new ( scope, context) ;
932+
933+ // Verify snapshot defaults are present
934+ let check = v8:: String :: new ( scope, "__snapshotCwd" ) . unwrap ( ) ;
935+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
936+ let result = script. run ( scope) . unwrap ( ) ;
937+ assert_eq ! ( result. to_rust_string_lossy( scope) , "/" ) ;
938+
939+ // Create a V8 payload to override _processConfig
940+ let payload_code = r#"({
941+ processConfig: { cwd: "/app", env: { FOO: "bar" }, timing_mitigation: "off", frozen_time_ms: null },
942+ osConfig: { homedir: "/home/user", tmpdir: "/tmp", platform: "linux", arch: "arm64" }
943+ })"# ;
944+ let payload_source = v8:: String :: new ( scope, payload_code) . unwrap ( ) ;
945+ let payload_script = v8:: Script :: compile ( scope, payload_source, None ) . unwrap ( ) ;
946+ let payload_val = payload_script. run ( scope) . unwrap ( ) ;
947+ let payload_bytes = serialize_v8_value ( scope, payload_val)
948+ . expect ( "serialize payload" ) ;
949+
950+ // Inject per-session globals (overrides snapshot defaults)
951+ crate :: execution:: inject_globals_from_payload ( scope, & payload_bytes) ;
952+
953+ // Verify _processConfig was overridden
954+ let check = v8:: String :: new ( scope, "_processConfig.cwd" ) . unwrap ( ) ;
955+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
956+ let result = script. run ( scope) . unwrap ( ) ;
957+ assert_eq ! (
958+ result. to_rust_string_lossy( scope) ,
959+ "/app" ,
960+ "_processConfig.cwd should be overridden from '/' to '/app'"
961+ ) ;
962+
963+ // Verify _osConfig was overridden
964+ let check = v8:: String :: new ( scope, "_osConfig.arch" ) . unwrap ( ) ;
965+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
966+ let result = script. run ( scope) . unwrap ( ) ;
967+ assert_eq ! (
968+ result. to_rust_string_lossy( scope) ,
969+ "arm64" ,
970+ "_osConfig.arch should be overridden to 'arm64'"
971+ ) ;
972+ }
973+
974+ // --- Part 20: Multiple restores from same snapshot are independent ---
975+ // Verifies that user code in one restored context does not leak to another.
976+ {
977+ let bridge_code = r#"
978+ (function() {
979+ globalThis.__bridge_ok = true;
980+ })();
981+ "# ;
982+ let blob = create_snapshot ( bridge_code) . expect ( "snapshot creation" ) ;
983+ let blob_bytes: Vec < u8 > = blob. to_vec ( ) ;
984+
985+ // Restore A: set a session-specific global
986+ {
987+ let mut isolate = create_isolate_from_snapshot ( blob_bytes. clone ( ) , None ) ;
988+ let scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
989+ let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
990+ let scope = & mut v8:: ContextScope :: new ( scope, context) ;
991+
992+ // Bridge state from snapshot should be present
993+ let check = v8:: String :: new ( scope, "String(__bridge_ok)" ) . unwrap ( ) ;
994+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
995+ let result = script. run ( scope) . unwrap ( ) ;
996+ assert_eq ! ( result. to_rust_string_lossy( scope) , "true" ) ;
997+
998+ // Set session-specific state
999+ let code = v8:: String :: new ( scope, "globalThis.__user_data = 'session-a';" ) . unwrap ( ) ;
1000+ let script = v8:: Script :: compile ( scope, code, None ) . unwrap ( ) ;
1001+ script. run ( scope) ;
1002+ }
1003+
1004+ // Restore B: session A's state should not be visible
1005+ {
1006+ let mut isolate = create_isolate_from_snapshot ( blob_bytes. clone ( ) , None ) ;
1007+ let scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
1008+ let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
1009+ let scope = & mut v8:: ContextScope :: new ( scope, context) ;
1010+
1011+ // Bridge state should still be present
1012+ let check = v8:: String :: new ( scope, "String(__bridge_ok)" ) . unwrap ( ) ;
1013+ let script = v8:: Script :: compile ( scope, check, None ) . unwrap ( ) ;
1014+ let result = script. run ( scope) . unwrap ( ) ;
1015+ assert_eq ! ( result. to_rust_string_lossy( scope) , "true" ) ;
1016+
1017+ // Session A's data should NOT be visible
1018+ let check = v8:: String :: new ( scope, "typeof __user_data" ) . unwrap ( ) ;
1019+ let script = v8:: Script :: compile ( scope, check, 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 user data"
1025+ ) ;
1026+ }
1027+ }
8251028 }
8261029}
0 commit comments