@@ -8,6 +8,12 @@ type LogPollingCtor = typeof import('./logs-polling-utilities').default;
88jest . mock ( '@contentstack/cli-utilities' , ( ) => cliUtilitiesJestMock ) ;
99jest . mock ( 'timers/promises' , ( ) => ( { setTimeout : jest . fn ( ) . mockResolvedValue ( undefined ) } ) ) ;
1010
11+ const CONFIG = {
12+ deployment : 'd1' ,
13+ environment : 'e1' ,
14+ pollingInterval : 1000 ,
15+ } ;
16+
1117function makeWatchQuery ( ) {
1218 let subscriber : ( result : any ) => void = ( ) => { } ;
1319 return {
@@ -21,11 +27,12 @@ function makeWatchQuery() {
2127 } ;
2228}
2329
24- const CONFIG = {
25- deployment : 'd1' ,
26- environment : 'e1' ,
27- pollingInterval : 1000 ,
28- } ;
30+ function page ( logs : any [ ] , pageInfo : Record < string , unknown > = { } ) {
31+ return {
32+ logs,
33+ pageInfo : { hasNewer : null , newestCursor : null , ...pageInfo } ,
34+ } ;
35+ }
2936
3037function getDeploymentStatus ( LogPollingClass : LogPollingCtor , watchQuery : jest . Mock ) : void {
3138 new LogPollingClass ( {
@@ -195,3 +202,125 @@ describe('cancelled deployment stops log polling', () => {
195202 expect ( defaultConfig . deploymentStatus ) . toContain ( 'CANCELLED' ) ;
196203 } ) ;
197204} ) ;
205+
206+ describe ( 'deployment logs use cursor paging (getDeploymentLogsV2)' , ( ) => {
207+ function buildInstance ( deploymentStatus : string [ ] = [ 'DEPLOYED' ] ) {
208+ const statusWatchQuery = makeWatchQuery ( ) ;
209+ const logsWatchQuery = makeWatchQuery ( ) ;
210+ const fallbackWatchQuery = makeWatchQuery ( ) ;
211+ const logsClientWatchQuery = jest
212+ . fn ( )
213+ . mockReturnValueOnce ( logsWatchQuery )
214+ . mockReturnValue ( fallbackWatchQuery ) ;
215+ const instance = new LogPolling ( {
216+ apolloManageClient : { watchQuery : jest . fn ( ) . mockReturnValue ( statusWatchQuery ) } as any ,
217+ apolloLogsClient : { watchQuery : logsClientWatchQuery } as any ,
218+ config : { deployment : 'd1' , environment : 'e1' , pollingInterval : 1000 , deploymentStatus } as any ,
219+ $event : new EventEmitter ( ) ,
220+ } ) ;
221+ return { instance, statusWatchQuery, logsWatchQuery, fallbackWatchQuery, logsClientWatchQuery } ;
222+ }
223+
224+ it ( 'opens with sortDirection desc and no cursor, tailing the newest page like the legacy query did' , async ( ) => {
225+ const { instance, logsClientWatchQuery } = buildInstance ( ) ;
226+
227+ await instance . deploymentLogs ( ) ;
228+
229+ const { query } = logsClientWatchQuery . mock . calls [ 0 ] [ 0 ] . variables ;
230+ expect ( query ) . toEqual ( { deploymentUid : 'd1' , limit : 5000 , sortDirection : 'desc' } ) ;
231+ expect ( query ) . not . toHaveProperty ( 'cursor' ) ;
232+ } ) ;
233+
234+ it ( 'advances by cursor in asc order — never by timestamp' , async ( ) => {
235+ const { instance, statusWatchQuery, logsWatchQuery } = buildInstance ( [ 'DEPLOYED' ] ) ;
236+
237+ await instance . deploymentLogs ( ) ;
238+ statusWatchQuery . emit ( { data : { Deployment : { status : 'LIVE' } } } ) ;
239+ await logsWatchQuery . emit ( {
240+ data : {
241+ getDeploymentLogsV2 : page ( [ { message : 'build started' , timestamp : '2026-08-06T10:00:00.123Z' } ] , {
242+ newestCursor : '[1775462400123,"abc"]' ,
243+ } ) ,
244+ } ,
245+ } ) ;
246+
247+ expect ( logsWatchQuery . setVariables ) . toHaveBeenCalledWith ( {
248+ query : {
249+ deploymentUid : 'd1' ,
250+ limit : 5000 ,
251+ sortDirection : 'asc' ,
252+ cursor : '[1775462400123,"abc"]' ,
253+ } ,
254+ } ) ;
255+ } ) ;
256+
257+ it ( 'does not re-arm when the cursor has not moved, so a repeated page cannot loop forever' , async ( ) => {
258+ const { instance, statusWatchQuery, logsWatchQuery } = buildInstance ( [ 'DEPLOYED' ] ) ;
259+
260+ await instance . deploymentLogs ( ) ;
261+ statusWatchQuery . emit ( { data : { Deployment : { status : 'LIVE' } } } ) ;
262+ const samePage = {
263+ data : {
264+ getDeploymentLogsV2 : page ( [ { message : 'x' , timestamp : '2026-08-06T10:00:00.000Z' } ] , {
265+ newestCursor : 'c1' ,
266+ } ) ,
267+ } ,
268+ } ;
269+ await logsWatchQuery . emit ( samePage ) ;
270+ await logsWatchQuery . emit ( samePage ) ;
271+
272+ expect ( logsWatchQuery . setVariables ) . toHaveBeenCalledTimes ( 1 ) ;
273+ } ) ;
274+
275+ it ( 'keeps draining past a terminal status while hasNewer reports another page' , async ( ) => {
276+ const { instance, statusWatchQuery, logsWatchQuery } = buildInstance ( [ 'DEPLOYED' ] ) ;
277+
278+ await instance . deploymentLogs ( ) ;
279+ statusWatchQuery . emit ( { data : { Deployment : { status : 'DEPLOYED' } } } ) ;
280+ await logsWatchQuery . emit ( {
281+ data : { getDeploymentLogsV2 : page ( [ { message : 'a' , timestamp : 'x' } ] , { hasNewer : true , newestCursor : 'c1' } ) } ,
282+ } ) ;
283+
284+ expect ( logsWatchQuery . stopPolling ) . not . toHaveBeenCalled ( ) ;
285+
286+ await logsWatchQuery . emit ( {
287+ data : { getDeploymentLogsV2 : page ( [ { message : 'b' , timestamp : 'y' } ] , { hasNewer : false , newestCursor : 'c2' } ) } ,
288+ } ) ;
289+
290+ expect ( logsWatchQuery . stopPolling ) . toHaveBeenCalledTimes ( 1 ) ;
291+ } ) ;
292+
293+ it ( 'falls back to the legacy getLogs query when the region has no V2 field' , async ( ) => {
294+ const { instance, logsWatchQuery, fallbackWatchQuery, logsClientWatchQuery } = buildInstance ( ) ;
295+ const errors : any [ ] = [ ] ;
296+ ( instance as any ) . $event . on ( 'deployment-logs' , ( e : any ) => {
297+ if ( e . msgType === 'error' ) errors . push ( e . message ) ;
298+ } ) ;
299+
300+ await instance . deploymentLogs ( ) ;
301+ await logsWatchQuery . emit ( {
302+ data : null ,
303+ error : { message : 'Cannot query field "getDeploymentLogsV2" on type "Query".' } ,
304+ } ) ;
305+
306+ expect ( logsWatchQuery . stopPolling ) . toHaveBeenCalledTimes ( 1 ) ;
307+ expect ( logsClientWatchQuery ) . toHaveBeenCalledTimes ( 2 ) ;
308+ expect ( logsClientWatchQuery . mock . calls [ 1 ] [ 0 ] . variables ) . toEqual ( { deploymentUid : 'd1' } ) ;
309+ expect ( fallbackWatchQuery . subscribe ) . toHaveBeenCalledTimes ( 1 ) ;
310+ expect ( errors ) . toHaveLength ( 0 ) ;
311+ } ) ;
312+
313+ it ( 'does not demote to the legacy query on a transient network error' , async ( ) => {
314+ const { instance, logsWatchQuery, logsClientWatchQuery } = buildInstance ( ) ;
315+ const errors : any [ ] = [ ] ;
316+ ( instance as any ) . $event . on ( 'deployment-logs' , ( e : any ) => {
317+ if ( e . msgType === 'error' ) errors . push ( e . message ) ;
318+ } ) ;
319+
320+ await instance . deploymentLogs ( ) ;
321+ await logsWatchQuery . emit ( { data : null , error : { message : 'Failed to fetch' } } ) ;
322+
323+ expect ( logsClientWatchQuery ) . toHaveBeenCalledTimes ( 1 ) ;
324+ expect ( errors ) . toContain ( 'Failed to fetch' ) ;
325+ } ) ;
326+ } ) ;
0 commit comments