@@ -1354,6 +1354,129 @@ describe("Proxy invariant correctness", () => {
13541354 expect ( 2 in list ) . toBe ( false ) ;
13551355 expect ( Object . keys ( list ) . filter ( k => k !== "length" ) ) . not . toContain ( "2" ) ;
13561356 } ) ;
1357+
1358+ test ( "getOwnPropertyDescriptor reports the written value, not the stale base value" , ( ) => {
1359+ const [ state , setState ] = createStore ( { count : 1 } ) ;
1360+ setState ( s => {
1361+ s . count ++ ;
1362+ } ) ;
1363+ flush ( ) ;
1364+ expect ( state . count ) . toBe ( 2 ) ;
1365+ expect ( Object . getOwnPropertyDescriptor ( state , "count" ) ?. value ) . toBe ( 2 ) ;
1366+
1367+ // and it must keep agreeing on later writes, not just the first
1368+ setState ( s => {
1369+ s . count ++ ;
1370+ } ) ;
1371+ flush ( ) ;
1372+ expect ( state . count ) . toBe ( 3 ) ;
1373+ expect ( Object . getOwnPropertyDescriptor ( state , "count" ) ?. value ) . toBe ( 3 ) ;
1374+ } ) ;
1375+
1376+ test ( "getOwnPropertyDescriptors agrees with reads for string and symbol keys" , ( ) => {
1377+ const sym = Symbol ( "count" ) ;
1378+ const [ state , setState ] = createStore ( { a : "old" , b : 1 , [ sym ] : 1 } ) ;
1379+ setState ( s => {
1380+ s . a = "new" ;
1381+ s [ sym ] = 2 ;
1382+ } ) ;
1383+ flush ( ) ;
1384+ const descs = Object . getOwnPropertyDescriptors ( state ) ;
1385+ expect ( state [ sym ] ) . toBe ( 2 ) ;
1386+ expect ( descs . a . value ) . toBe ( "new" ) ;
1387+ expect ( descs . a . enumerable ) . toBe ( true ) ;
1388+ expect ( descs . b . value ) . toBe ( 1 ) ;
1389+ expect ( Object . getOwnPropertyDescriptor ( state , sym ) ?. value ) . toBe ( 2 ) ;
1390+ expect ( descs [ sym ] . value ) . toBe ( 2 ) ;
1391+ } ) ;
1392+
1393+ test ( "descriptor behavior for added and deleted keys is unchanged" , ( ) => {
1394+ const [ state , setState ] = createStore < Record < string , number > > ( { kept : 1 , dropped : 2 } ) ;
1395+ setState ( s => {
1396+ s . added = 3 ;
1397+ delete s . dropped ;
1398+ } ) ;
1399+ flush ( ) ;
1400+ expect ( Object . getOwnPropertyDescriptor ( state , "added" ) ?. value ) . toBe ( 3 ) ;
1401+ expect ( Object . getOwnPropertyDescriptor ( state , "dropped" ) ) . toBeUndefined ( ) ;
1402+ expect ( Object . getOwnPropertyDescriptor ( state , "kept" ) ?. value ) . toBe ( 1 ) ;
1403+ } ) ;
1404+
1405+ test ( "written non-configurable properties keep satisfying the proxy invariant" , ( ) => {
1406+ // The proxy target is the internal node object, not the source object.
1407+ const source = { } ;
1408+ Object . defineProperty ( source , "locked" , {
1409+ value : 1 ,
1410+ enumerable : true ,
1411+ writable : false ,
1412+ configurable : false
1413+ } ) ;
1414+ const [ state , setState ] = createStore < { locked : number } > ( source ) ;
1415+ setState ( s => {
1416+ s . locked = 2 ;
1417+ } ) ;
1418+ flush ( ) ;
1419+ expect ( state . locked ) . toBe ( 2 ) ;
1420+ expect ( ( ) => Object . keys ( state ) ) . not . toThrow ( ) ;
1421+ expect ( ( ) => ( { ...state } ) ) . not . toThrow ( ) ;
1422+ const desc = Object . getOwnPropertyDescriptor ( state , "locked" ) ;
1423+ expect ( desc ?. value ) . toBe ( 2 ) ;
1424+ expect ( desc ?. configurable ) . toBe ( true ) ;
1425+ } ) ;
1426+
1427+ test ( "writes preserve the base descriptor's structure, matching plain assignment semantics" , ( ) => {
1428+ const source = { visible : 1 } ;
1429+ Object . defineProperty ( source , "hidden" , {
1430+ value : 10 ,
1431+ enumerable : false ,
1432+ writable : true ,
1433+ configurable : true
1434+ } ) ;
1435+ const [ state , setState ] = createStore < { visible : number ; hidden : number } > ( source ) ;
1436+ setState ( s => {
1437+ s . hidden = 20 ;
1438+ } ) ;
1439+ flush ( ) ;
1440+ expect ( state . hidden ) . toBe ( 20 ) ;
1441+ const desc = Object . getOwnPropertyDescriptor ( state , "hidden" ) ;
1442+ expect ( desc ?. value ) . toBe ( 20 ) ;
1443+ expect ( desc ?. enumerable ) . toBe ( false ) ;
1444+ expect ( Object . keys ( state ) ) . toEqual ( [ "visible" ] ) ;
1445+ } ) ;
1446+
1447+ test ( "written accessor properties keep accessor descriptors" , ( ) => {
1448+ const source = {
1449+ _count : 1 ,
1450+ get count ( ) {
1451+ return this . _count ;
1452+ } ,
1453+ set count ( value : number ) {
1454+ this . _count = value ;
1455+ }
1456+ } ;
1457+ const [ state , setState ] = createStore ( source ) ;
1458+ setState ( s => {
1459+ s . count = 2 ;
1460+ } ) ;
1461+ flush ( ) ;
1462+ const desc = Object . getOwnPropertyDescriptor ( state , "count" ) ;
1463+ expect ( desc ?. get ) . toBe ( Object . getOwnPropertyDescriptor ( source , "count" ) ?. get ) ;
1464+ expect ( desc ?. set ) . toBe ( Object . getOwnPropertyDescriptor ( source , "count" ) ?. set ) ;
1465+ expect ( "value" in desc ! ) . toBe ( false ) ;
1466+ } ) ;
1467+
1468+ test ( "writing over an inherited property yields an own data descriptor and a safe snapshot" , ( ) => {
1469+ // This used to return no descriptor, which also crashed snapshot().
1470+ const proto = { label : "proto" } ;
1471+ const [ state , setState ] = createStore < { label : string } > ( Object . create ( proto ) ) ;
1472+ setState ( s => {
1473+ s . label = "own" ;
1474+ } ) ;
1475+ flush ( ) ;
1476+ expect ( state . label ) . toBe ( "own" ) ;
1477+ expect ( Object . getOwnPropertyDescriptor ( state , "label" ) ?. value ) . toBe ( "own" ) ;
1478+ expect ( snapshot ( state ) . label ) . toBe ( "own" ) ;
1479+ } ) ;
13571480} ) ;
13581481
13591482describe ( "Store key handling and tracked truncation" , ( ) => {
0 commit comments