@@ -3076,3 +3076,211 @@ describe("CloudTaskService", () => {
30763076 ) . toHaveLength ( 3 ) ;
30773077 } ) ;
30783078} ) ;
3079+
3080+ describe ( "CloudTaskService MCP relay" , ( ) => {
3081+ let relayService : CloudTaskService ;
3082+ let mcpRelayExecutor : {
3083+ execute : ReturnType < typeof vi . fn > ;
3084+ closeRun : ReturnType < typeof vi . fn > ;
3085+ } ;
3086+
3087+ beforeEach ( ( ) => {
3088+ const scopedLog = {
3089+ debug : vi . fn ( ) ,
3090+ info : vi . fn ( ) ,
3091+ warn : vi . fn ( ) ,
3092+ error : vi . fn ( ) ,
3093+ } ;
3094+ const loggerMock = { ...scopedLog , scope : vi . fn ( ( ) => scopedLog ) } ;
3095+ const analyticsMock = { track : vi . fn ( ) } ;
3096+ mcpRelayExecutor = {
3097+ execute : vi . fn ( async ( ) => ( {
3098+ payload : { jsonrpc : "2.0" , id : 1 , result : { } } ,
3099+ } ) ) ,
3100+ closeRun : vi . fn ( async ( ) => { } ) ,
3101+ } ;
3102+ relayService = new CloudTaskService (
3103+ mockAuthService as never ,
3104+ analyticsMock as never ,
3105+ loggerMock ,
3106+ mcpRelayExecutor as never ,
3107+ ) ;
3108+
3109+ mockNetFetch . mockReset ( ) ;
3110+ mockStreamFetch . mockReset ( ) ;
3111+ mockStreamTokenFetch . mockReset ( ) ;
3112+ mockStreamTokenFetch . mockImplementation ( ( ) =>
3113+ Promise . resolve (
3114+ createJsonResponse ( { token : "test-token" , stream_base_url : null } ) ,
3115+ ) ,
3116+ ) ;
3117+ mockAuthService . authenticatedFetch . mockReset ( ) ;
3118+ vi . stubGlobal ( "fetch" , fetchRouter ) ;
3119+ mockAuthService . authenticatedFetch . mockImplementation (
3120+ async ( input : string | Request , init ?: RequestInit ) => {
3121+ return fetchRouter ( input , {
3122+ ...init ,
3123+ headers : {
3124+ ...( init ?. headers ?? { } ) ,
3125+ Authorization : "Bearer token" ,
3126+ } ,
3127+ } ) ;
3128+ } ,
3129+ ) ;
3130+ } ) ;
3131+
3132+ afterEach ( ( ) => {
3133+ relayService . unwatchAll ( ) ;
3134+ vi . unstubAllGlobals ( ) ;
3135+ } ) ;
3136+
3137+ function mcpRequestSseLine (
3138+ overrides : Partial < {
3139+ requestId : string ;
3140+ server : string ;
3141+ expiresAt : string ;
3142+ } > = { } ,
3143+ ) : string {
3144+ const event = {
3145+ type : "mcp_request" ,
3146+ requestId : overrides . requestId ?? "req-1" ,
3147+ server : overrides . server ?? "slack" ,
3148+ payload : { jsonrpc : "2.0" , id : 1 , method : "initialize" } ,
3149+ expiresAt :
3150+ overrides . expiresAt ?? new Date ( Date . now ( ) + 60_000 ) . toISOString ( ) ,
3151+ } ;
3152+ return `data: ${ JSON . stringify ( event ) } \n\n` ;
3153+ }
3154+
3155+ function watchRun ( runId : string ) : void {
3156+ mockNetFetch . mockResolvedValueOnce (
3157+ createJsonResponse ( {
3158+ id : runId ,
3159+ status : "in_progress" ,
3160+ stage : null ,
3161+ output : null ,
3162+ error_message : null ,
3163+ branch : "main" ,
3164+ updated_at : "2026-01-01T00:00:00Z" ,
3165+ } ) ,
3166+ ) ;
3167+ relayService . watch ( {
3168+ taskId : "task-1" ,
3169+ runId,
3170+ apiHost : "https://app.example.com" ,
3171+ teamId : 2 ,
3172+ resumeFromEntryCount : 0 ,
3173+ } ) ;
3174+ }
3175+
3176+ it ( "drops a relay request for a server the run never designated" , async ( ) => {
3177+ mockStreamFetch . mockResolvedValueOnce (
3178+ createOpenSseResponse ( mcpRequestSseLine ( { server : "slack" } ) ) ,
3179+ ) ;
3180+ relayService . designateRelayedMcpServers ( "run-1" , [ "grafana" ] ) ;
3181+ watchRun ( "run-1" ) ;
3182+
3183+ await waitFor ( ( ) => mockStreamFetch . mock . calls . length > 0 ) ;
3184+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3185+
3186+ expect ( mcpRelayExecutor . execute ) . not . toHaveBeenCalled ( ) ;
3187+ } ) ;
3188+
3189+ it ( "executes a designated relay request exactly once and posts mcp_response" , async ( ) => {
3190+ mockStreamFetch . mockResolvedValueOnce (
3191+ createOpenSseResponse (
3192+ mcpRequestSseLine ( { requestId : "req-1" , server : "slack" } ) +
3193+ mcpRequestSseLine ( { requestId : "req-1" , server : "slack" } ) ,
3194+ ) ,
3195+ ) ;
3196+ mockNetFetch . mockResolvedValueOnce ( createJsonResponse ( { result : { } } ) ) ;
3197+ relayService . designateRelayedMcpServers ( "run-1" , [ "slack" ] ) ;
3198+ watchRun ( "run-1" ) ;
3199+
3200+ await waitFor ( ( ) => mcpRelayExecutor . execute . mock . calls . length > 0 ) ;
3201+ // The duplicate line above must not trigger a second execution.
3202+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3203+
3204+ expect ( mcpRelayExecutor . execute ) . toHaveBeenCalledOnce ( ) ;
3205+ expect ( mcpRelayExecutor . execute ) . toHaveBeenCalledWith (
3206+ "run-1" ,
3207+ "slack" ,
3208+ expect . objectContaining ( { method : "initialize" } ) ,
3209+ ) ;
3210+
3211+ await waitFor ( ( ) =>
3212+ mockNetFetch . mock . calls . some ( ( [ url ] ) =>
3213+ ( url as string ) . includes ( "/command/" ) ,
3214+ ) ,
3215+ ) ;
3216+ const commandCall = mockNetFetch . mock . calls . find ( ( [ url ] ) =>
3217+ ( url as string ) . includes ( "/command/" ) ,
3218+ ) ;
3219+ const body = JSON . parse ( ( commandCall ?. [ 1 ] as RequestInit ) . body as string ) ;
3220+ expect ( body ) . toEqual (
3221+ expect . objectContaining ( {
3222+ method : "mcp_response" ,
3223+ params : {
3224+ requestId : "req-1" ,
3225+ server : "slack" ,
3226+ payload : { jsonrpc : "2.0" , id : 1 , result : { } } ,
3227+ } ,
3228+ } ) ,
3229+ ) ;
3230+ } ) ;
3231+
3232+ it ( "evicts a run's relay designation once the run reaches a terminal status" , async ( ) => {
3233+ mockStreamFetch . mockResolvedValueOnce (
3234+ createOpenSseResponse (
3235+ `data: ${ JSON . stringify ( {
3236+ type : "task_run_state" ,
3237+ status : "completed" ,
3238+ updated_at : "2026-01-01T00:00:01Z" ,
3239+ } ) } \n\n${ mcpRequestSseLine ( { server : "slack" } ) } `,
3240+ ) ,
3241+ ) ;
3242+ relayService . designateRelayedMcpServers ( "run-1" , [ "slack" ] ) ;
3243+ watchRun ( "run-1" ) ;
3244+
3245+ await waitFor ( ( ) => mockStreamFetch . mock . calls . length > 0 ) ;
3246+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3247+
3248+ expect ( mcpRelayExecutor . execute ) . not . toHaveBeenCalled ( ) ;
3249+ } ) ;
3250+
3251+ it ( "drops an expired relay request without executing it" , async ( ) => {
3252+ mockStreamFetch . mockResolvedValueOnce (
3253+ createOpenSseResponse (
3254+ mcpRequestSseLine ( {
3255+ server : "slack" ,
3256+ expiresAt : new Date ( Date . now ( ) - 60_000 ) . toISOString ( ) ,
3257+ } ) ,
3258+ ) ,
3259+ ) ;
3260+ relayService . designateRelayedMcpServers ( "run-1" , [ "slack" ] ) ;
3261+ watchRun ( "run-1" ) ;
3262+
3263+ await waitFor ( ( ) => mockStreamFetch . mock . calls . length > 0 ) ;
3264+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3265+
3266+ expect ( mcpRelayExecutor . execute ) . not . toHaveBeenCalled ( ) ;
3267+ } ) ;
3268+
3269+ it ( "sends no mcp_response for a fire-and-forget relay execution" , async ( ) => {
3270+ mcpRelayExecutor . execute . mockResolvedValueOnce ( { } ) ;
3271+ mockStreamFetch . mockResolvedValueOnce (
3272+ createOpenSseResponse ( mcpRequestSseLine ( { server : "slack" } ) ) ,
3273+ ) ;
3274+ relayService . designateRelayedMcpServers ( "run-1" , [ "slack" ] ) ;
3275+ watchRun ( "run-1" ) ;
3276+
3277+ await waitFor ( ( ) => mcpRelayExecutor . execute . mock . calls . length > 0 ) ;
3278+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
3279+
3280+ expect (
3281+ mockNetFetch . mock . calls . some ( ( [ url ] ) =>
3282+ ( url as string ) . includes ( "/command/" ) ,
3283+ ) ,
3284+ ) . toBe ( false ) ;
3285+ } ) ;
3286+ } ) ;
0 commit comments