88import { authMock , authMockFns } from '@sim/testing/mocks/auth.mock'
99import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from 'vitest'
1010
11- const { redisUrl, inheritedEnv } = await vi . hoisted ( async ( ) => {
11+ const { redisUrl, inheritedEnv, worker } = await vi . hoisted ( async ( ) => {
1212 const { readTestRedisUrl } = await import ( '@sim/db/testing/test-infrastructure' )
13+ const { createServer } = await import ( 'node:http' )
14+ /** Stands in for the agent worker, which Stop also tells to end the stream. */
15+ const server = createServer ( async ( request , response ) => {
16+ for await ( const _chunk of request ) {
17+ }
18+ response . writeHead ( 200 , { 'content-type' : 'application/json' } )
19+ response . end ( JSON . stringify ( { settled : true } ) )
20+ } )
21+ await new Promise < void > ( ( resolve ) => server . listen ( 0 , '127.0.0.1' , resolve ) )
22+ const { port } = server . address ( ) as { port : number }
1323 const url = readTestRedisUrl ( )
1424 const inheritedEnv = {
1525 REDIS_URL : process . env . REDIS_URL ,
1626 COPILOT_TOOL_PERMISSIONS_ENABLED : process . env . COPILOT_TOOL_PERMISSIONS_ENABLED ,
27+ SIM_AGENT_API_URL : process . env . SIM_AGENT_API_URL ,
1728 }
18- /** The real Redis module, the confirmation channel and the permission flag read these at import. */
29+ /** The real Redis module, the confirmation channel, the permission flag and worker URL read these at import. */
1930 if ( url ) process . env . REDIS_URL = url
2031 process . env . COPILOT_TOOL_PERMISSIONS_ENABLED = 'true'
21- return { redisUrl : url , inheritedEnv }
32+ process . env . SIM_AGENT_API_URL = `http://127.0.0.1:${ port } `
33+ return { redisUrl : url , inheritedEnv, worker : { server } }
2234} )
2335
2436vi . mock ( '@/lib/auth' , ( ) => authMock )
@@ -31,6 +43,7 @@ import {
3143 copilotChats ,
3244 copilotRuns ,
3345 desktopDevices ,
46+ permissions ,
3447 session ,
3548 user ,
3649 workspace ,
@@ -59,9 +72,9 @@ import {
5972 areStreamToolExecutionsSettled ,
6073 claimToolExecution ,
6174 prepareWorkbenchAccess ,
62- requestRunStop ,
6375 revokeExpiredSimToolExecutions ,
6476} from '@/lib/mothership/async-runs/repository'
77+ import { abortRun } from '@/lib/mothership/request/application/controls'
6578import { prePersistClientExecutableToolCall , sseHandlers } from '@/lib/mothership/request/handlers'
6679import { waitForClientToolCompletion } from '@/lib/mothership/request/tools/client'
6780import { TraceCollector } from '@/lib/mothership/request/trace'
@@ -125,6 +138,7 @@ afterAll(async () => {
125138 channels [ name ] = undefined
126139 }
127140 await closeRedisConnection ( )
141+ await new Promise < void > ( ( resolve ) => worker . server . close ( ( ) => resolve ( ) ) )
128142 for ( const [ key , value ] of Object . entries ( inheritedEnv ) ) {
129143 if ( value === undefined ) delete process . env [ key ]
130144 else process . env [ key ] = value
@@ -328,6 +342,13 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
328342 ownerId : userId ,
329343 billedAccountUserId : userId ,
330344 } )
345+ await db . insert ( permissions ) . values ( {
346+ id : generateId ( ) ,
347+ userId,
348+ entityType : 'workspace' ,
349+ entityId : workspaceId ,
350+ permissionType : 'admin' ,
351+ } )
331352 authMockFns . mockGetSession . mockResolvedValue ( {
332353 user : { id : userId , email : `${ userId } @bound-turn.test` , name : 'Bound turn' } ,
333354 session : { id : generateId ( ) , userId } ,
@@ -541,7 +562,7 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
541562 )
542563
543564 it (
544- 'ends a running call on Stop, and the device learns its token was revoked ' ,
565+ 'ends a running call on Stop without the device, and tells the device to cancel it ' ,
545566 async ( ) => {
546567 const desktop = await signedInDesktop ( )
547568 const run = await boundRun ( desktop )
@@ -553,8 +574,16 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
553574 } )
554575 await offered ( toolCallId )
555576 const { executionToken } = await desktop . claim ( toolCallId )
556- await requestRunStop ( { userId, workspaceId, streamId : run . streamId } )
577+ await abortRun . execute ( {
578+ principal : { kind : 'session' , userId, sessionId : generateId ( ) } ,
579+ input : { streamId : run . streamId , chatId : run . chatId , workspaceId } ,
580+ } )
557581 await answer
582+ /** Stop rings the device to cancel what it runs; it never acknowledges here. */
583+ await until (
584+ async ( ) => [ ...desktop . rings ] ,
585+ ( rings ) => rings . includes ( 'cancel' )
586+ )
558587
559588 expect ( resultOf ( context , toolCallId ) ) . toMatchObject ( {
560589 success : false ,
@@ -611,7 +640,6 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
611640
612641 await lapse ( toolCallId , 'pickup' )
613642 await answer
614- expect ( reads ) . toHaveBeenCalled ( )
615643 expect ( resultOf ( context , toolCallId ) ) . toMatchObject ( {
616644 success : false ,
617645 output : { notStarted : true , reason : 'not_responding' } ,
0 commit comments