@@ -1636,7 +1636,138 @@ describe('settingsStore', () => {
16361636 } ) ;
16371637
16381638 // ========================================================================
1639- // 13. Non-React Access
1639+ // 13. setPersistentWebLink race-condition and rollback tests
1640+ // ========================================================================
1641+
1642+ describe ( 'setPersistentWebLink' , ( ) => {
1643+ beforeEach ( ( ) => {
1644+ useSettingsStore . setState ( { persistentWebLink : false } ) ;
1645+ } ) ;
1646+
1647+ it ( 'should optimistically set persistentWebLink to true and call persistCurrentToken' , async ( ) => {
1648+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1649+ await setPersistentWebLink ( true ) ;
1650+
1651+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( true ) ;
1652+ expect ( window . maestro . live . persistCurrentToken ) . toHaveBeenCalledOnce ( ) ;
1653+ } ) ;
1654+
1655+ it ( 'should rollback to false on soft IPC failure (result.success === false)' , async ( ) => {
1656+ vi . mocked ( window . maestro . live . persistCurrentToken ) . mockResolvedValueOnce ( {
1657+ success : false ,
1658+ message : 'Web server is not running.' ,
1659+ } ) ;
1660+
1661+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1662+ await setPersistentWebLink ( true ) ;
1663+
1664+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( false ) ;
1665+ } ) ;
1666+
1667+ it ( 'should rollback to false on hard IPC failure (thrown exception)' , async ( ) => {
1668+ vi . mocked ( window . maestro . live . persistCurrentToken ) . mockRejectedValueOnce (
1669+ new Error ( 'IPC timeout' )
1670+ ) ;
1671+
1672+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1673+ await setPersistentWebLink ( true ) ;
1674+
1675+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( false ) ;
1676+ } ) ;
1677+
1678+ it ( 'should call clearPersistentToken when disabling' , async ( ) => {
1679+ useSettingsStore . setState ( { persistentWebLink : true } ) ;
1680+
1681+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1682+ await setPersistentWebLink ( false ) ;
1683+
1684+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( false ) ;
1685+ expect ( window . maestro . live . clearPersistentToken ) . toHaveBeenCalledOnce ( ) ;
1686+ } ) ;
1687+
1688+ it ( 'should rollback to true on clearPersistentToken hard failure (thrown exception)' , async ( ) => {
1689+ useSettingsStore . setState ( { persistentWebLink : true } ) ;
1690+ vi . mocked ( window . maestro . live . clearPersistentToken ) . mockRejectedValueOnce (
1691+ new Error ( 'IPC timeout' )
1692+ ) ;
1693+
1694+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1695+ await setPersistentWebLink ( false ) ;
1696+
1697+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( true ) ;
1698+ } ) ;
1699+
1700+ it ( 'should rollback to true on clearPersistentToken soft failure (result.success === false)' , async ( ) => {
1701+ useSettingsStore . setState ( { persistentWebLink : true } ) ;
1702+ vi . mocked ( window . maestro . live . clearPersistentToken ) . mockResolvedValueOnce ( {
1703+ success : false ,
1704+ message : 'Settings write failed.' ,
1705+ } as any ) ;
1706+
1707+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1708+ await setPersistentWebLink ( false ) ;
1709+
1710+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( true ) ;
1711+ } ) ;
1712+
1713+ it ( 'should handle rapid double-toggle (enable then disable) correctly' , async ( ) => {
1714+ // Simulate enable call that resolves slowly
1715+ let resolveEnable : ( value : any ) => void ;
1716+ const slowEnable = new Promise ( ( resolve ) => {
1717+ resolveEnable = resolve ;
1718+ } ) ;
1719+ vi . mocked ( window . maestro . live . persistCurrentToken ) . mockReturnValueOnce ( slowEnable as any ) ;
1720+
1721+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1722+
1723+ // Start enable (will be in-flight)
1724+ const enablePromise = setPersistentWebLink ( true ) ;
1725+ // Immediately disable (supersedes the enable)
1726+ const disablePromise = setPersistentWebLink ( false ) ;
1727+
1728+ // Resolve the slow enable after disable was called
1729+ resolveEnable ! ( { success : true } ) ;
1730+
1731+ await enablePromise ;
1732+ await disablePromise ;
1733+
1734+ // Final state should reflect the last user intent: disabled
1735+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( false ) ;
1736+ expect ( window . maestro . live . clearPersistentToken ) . toHaveBeenCalled ( ) ;
1737+ } ) ;
1738+
1739+ it ( 'should handle rapid reverse toggle (disable then enable) correctly' , async ( ) => {
1740+ // Start with enabled state
1741+ useSettingsStore . setState ( { persistentWebLink : true } ) ;
1742+
1743+ // Simulate disable call that resolves slowly
1744+ let resolveClear : ( value : any ) => void ;
1745+ const slowClear = new Promise ( ( resolve ) => {
1746+ resolveClear = resolve ;
1747+ } ) ;
1748+ vi . mocked ( window . maestro . live . clearPersistentToken ) . mockReturnValueOnce ( slowClear as any ) ;
1749+
1750+ const { setPersistentWebLink } = useSettingsStore . getState ( ) ;
1751+
1752+ // Start disable (will be in-flight)
1753+ const disablePromise = setPersistentWebLink ( false ) ;
1754+ // Immediately re-enable (supersedes the disable)
1755+ const enablePromise = setPersistentWebLink ( true ) ;
1756+
1757+ // Resolve the slow clear after enable was called
1758+ resolveClear ! ( { success : true } ) ;
1759+
1760+ await disablePromise ;
1761+ await enablePromise ;
1762+
1763+ // Final state should reflect the last user intent: enabled
1764+ expect ( useSettingsStore . getState ( ) . persistentWebLink ) . toBe ( true ) ;
1765+ expect ( window . maestro . live . persistCurrentToken ) . toHaveBeenCalled ( ) ;
1766+ } ) ;
1767+ } ) ;
1768+
1769+ // ========================================================================
1770+ // 14. Non-React Access
16401771 // ========================================================================
16411772
16421773 describe ( 'non-React access' , ( ) => {
0 commit comments