@@ -1480,6 +1480,208 @@ test('worker clears quarantine after a command cancellation confirms process ter
14801480 assert . deepEqual ( lifecycle , [ 'arm' , 'execute' , 'settle' , 'clear' ] ) ;
14811481} ) ;
14821482
1483+ test ( 'worker retries a clean Stop rejection near its deadline through the cancellation grace' , async ( ) => {
1484+ const lifecycle : string [ ] = [ ] ;
1485+ const settlements : Array < Record < string , unknown > > = [ ] ;
1486+ const remainingMs = 100 ;
1487+ const startedAt = Date . now ( ) ;
1488+ const baseCapabilities = {
1489+ protocolVersion : 1 as const ,
1490+ operations : [ 'read_file' as const ] ,
1491+ workspaces : [ { id : 'primary' , operations : [ 'read_file' as const ] } ] ,
1492+ } ;
1493+ const workspaceTools = new SandboxWorkspaceTools ( {
1494+ workspaceTools : {
1495+ capabilities : baseCapabilities ,
1496+ mutationFailuresAreAtomic : true ,
1497+ async execute ( ) { throw new Error ( 'base executor must not run' ) ; } ,
1498+ } ,
1499+ commandWorkspaces : [ 'primary' ] ,
1500+ commandSandbox : {
1501+ mutationFailuresAreAtomic : true ,
1502+ async execute ( _request , signal ) {
1503+ lifecycle . push ( 'execute' ) ;
1504+ await new Promise < void > ( ( resolve ) => {
1505+ if ( signal ?. aborted ) return resolve ( ) ;
1506+ signal ?. addEventListener ( 'abort' , ( ) => resolve ( ) , { once : true } ) ;
1507+ } ) ;
1508+ lifecycle . push ( 'stop' ) ;
1509+ // Process-group termination is confirmed after the original deadline.
1510+ await new Promise ( ( resolve ) => setTimeout ( resolve , remainingMs ) ) ;
1511+ throw new WorkspaceToolError (
1512+ 'Workspace command execution aborted' ,
1513+ 'EXECUTION_ABORTED' ,
1514+ true ,
1515+ false ,
1516+ ) ;
1517+ } ,
1518+ } ,
1519+ } ) ;
1520+ const worker = new BridgeWorker ( {
1521+ codeApiUrl : 'https://code.example/v1' ,
1522+ token : 'worker-secret' ,
1523+ workerId : 'vm-1' ,
1524+ incarnationId,
1525+ sandboxEndpoint : 'http://127.0.0.1:2000/api/v2' ,
1526+ capabilities : {
1527+ statefulWorkspace : true ,
1528+ sandboxProfile : 'nsjail' ,
1529+ runtimes : [ 'bash' ] ,
1530+ workspaceTools : workspaceTools . capabilities ,
1531+ } ,
1532+ workspaceTools,
1533+ workspaceMutationQuarantine : mutationQuarantine (
1534+ ( ) => lifecycle . push ( 'quarantine' ) ,
1535+ ( ) => lifecycle . push ( 'arm' ) ,
1536+ ( ) => lifecycle . push ( 'clear' ) ,
1537+ ) ,
1538+ cancellationPollIntervalMs : 5 ,
1539+ fetchImpl : async ( input , init ) => {
1540+ if ( String ( input ) . endsWith ( '/cancellation' ) ) {
1541+ return Response . json ( {
1542+ protocolVersion : 1 ,
1543+ cancelled : Date . now ( ) >= startedAt + remainingMs / 2 ,
1544+ } ) ;
1545+ }
1546+ if ( ! String ( input ) . endsWith ( '/settle' ) ) {
1547+ return Response . json ( { protocolVersion : 1 , accepted : true } ) ;
1548+ }
1549+ lifecycle . push ( 'settle' ) ;
1550+ settlements . push ( {
1551+ ...( JSON . parse ( String ( init ?. body ) ) as Record < string , unknown > ) ,
1552+ attemptedAt : Date . now ( ) ,
1553+ } ) ;
1554+ // Settlement delivery takes a real transport turn and honors its deadline.
1555+ await new Promise < void > ( ( resolve , reject ) => {
1556+ const timer = setTimeout ( resolve , 10 ) ;
1557+ init ?. signal ?. addEventListener (
1558+ 'abort' ,
1559+ ( ) => {
1560+ clearTimeout ( timer ) ;
1561+ reject ( new DOMException ( 'aborted' , 'AbortError' ) ) ;
1562+ } ,
1563+ { once : true } ,
1564+ ) ;
1565+ } ) ;
1566+ if ( settlements . length === 1 ) {
1567+ return Response . json (
1568+ { error : 'Bridge settlement temporarily unavailable' } ,
1569+ { status : 503 } ,
1570+ ) ;
1571+ }
1572+ return Response . json ( { protocolVersion : 1 , accepted : true } ) ;
1573+ } ,
1574+ } ) ;
1575+
1576+ await worker . executeAndSettle ( {
1577+ protocolVersion : 1 ,
1578+ assignmentId : 'assignment-command-stopped-near-deadline' ,
1579+ workerId : 'vm-1' ,
1580+ incarnationId,
1581+ generation : 4 ,
1582+ leaseToken : 'lease-token-that-is-long-enough-for-testing' ,
1583+ expiresAt : new Date ( startedAt + remainingMs ) . toISOString ( ) ,
1584+ remainingMs,
1585+ executionKind : 'workspace_tool' ,
1586+ request : {
1587+ protocolVersion : 1 ,
1588+ operation : 'execute_command' ,
1589+ workspaceId : 'primary' ,
1590+ command : 'sleep 30; touch delayed.txt' ,
1591+ } ,
1592+ } ) ;
1593+
1594+ assert . deepEqual ( lifecycle , [
1595+ 'arm' ,
1596+ 'execute' ,
1597+ 'stop' ,
1598+ 'settle' ,
1599+ 'settle' ,
1600+ 'clear' ,
1601+ ] ) ;
1602+ assert . ok ( Number ( settlements [ 0 ] ?. attemptedAt ) > startedAt + remainingMs ) ;
1603+ assert . equal ( settlements [ 1 ] ?. status , 'rejected' ) ;
1604+ assert . equal ( settlements [ 1 ] ?. errorCode , 'EXECUTION_ABORTED' ) ;
1605+ } ) ;
1606+
1607+ test ( 'worker keeps quarantine armed when shutdown interrupts a clean command rejection' , async ( ) => {
1608+ const lifecycle : string [ ] = [ ] ;
1609+ const controller = new AbortController ( ) ;
1610+ const baseCapabilities = {
1611+ protocolVersion : 1 as const ,
1612+ operations : [ 'read_file' as const ] ,
1613+ workspaces : [ { id : 'primary' , operations : [ 'read_file' as const ] } ] ,
1614+ } ;
1615+ const workspaceTools = new SandboxWorkspaceTools ( {
1616+ workspaceTools : {
1617+ capabilities : baseCapabilities ,
1618+ mutationFailuresAreAtomic : true ,
1619+ async execute ( ) { throw new Error ( 'base executor must not run' ) ; } ,
1620+ } ,
1621+ commandWorkspaces : [ 'primary' ] ,
1622+ commandSandbox : {
1623+ mutationFailuresAreAtomic : true ,
1624+ async execute ( ) {
1625+ lifecycle . push ( 'execute' ) ;
1626+ controller . abort ( new Error ( 'shutdown' ) ) ;
1627+ throw new WorkspaceToolError (
1628+ 'Workspace command execution aborted' ,
1629+ 'EXECUTION_ABORTED' ,
1630+ true ,
1631+ false ,
1632+ ) ;
1633+ } ,
1634+ } ,
1635+ } ) ;
1636+ const worker = new BridgeWorker ( {
1637+ codeApiUrl : 'https://code.example/v1' ,
1638+ token : 'worker-secret' ,
1639+ workerId : 'vm-1' ,
1640+ incarnationId,
1641+ sandboxEndpoint : 'http://127.0.0.1:2000/api/v2' ,
1642+ capabilities : {
1643+ statefulWorkspace : true ,
1644+ sandboxProfile : 'nsjail' ,
1645+ runtimes : [ 'bash' ] ,
1646+ workspaceTools : workspaceTools . capabilities ,
1647+ } ,
1648+ workspaceTools,
1649+ workspaceMutationQuarantine : mutationQuarantine (
1650+ ( ) => lifecycle . push ( 'quarantine' ) ,
1651+ ( ) => lifecycle . push ( 'arm' ) ,
1652+ ( ) => lifecycle . push ( 'clear' ) ,
1653+ ) ,
1654+ fetchImpl : async ( ) => {
1655+ lifecycle . push ( 'settle' ) ;
1656+ return Response . json ( { protocolVersion : 1 , accepted : true } ) ;
1657+ } ,
1658+ } ) ;
1659+
1660+ await assert . rejects (
1661+ worker . executeAndSettle (
1662+ {
1663+ protocolVersion : 1 ,
1664+ assignmentId : 'assignment-command-shutdown-cleanly' ,
1665+ workerId : 'vm-1' ,
1666+ incarnationId,
1667+ generation : 4 ,
1668+ leaseToken : 'lease-token-that-is-long-enough-for-testing' ,
1669+ expiresAt : new Date ( Date . now ( ) + 5_000 ) . toISOString ( ) ,
1670+ executionKind : 'workspace_tool' ,
1671+ request : {
1672+ protocolVersion : 1 ,
1673+ operation : 'execute_command' ,
1674+ workspaceId : 'primary' ,
1675+ command : 'sleep 30' ,
1676+ } ,
1677+ } ,
1678+ controller . signal ,
1679+ ) ,
1680+ / s h u t d o w n / ,
1681+ ) ;
1682+ assert . deepEqual ( lifecycle , [ 'arm' , 'execute' ] ) ;
1683+ } ) ;
1684+
14831685test ( 'worker retains quarantine when an atomic executor cannot confirm durability' , async ( ) => {
14841686 const lifecycle : string [ ] = [ ] ;
14851687 const workspaceCapabilities = {
0 commit comments