@@ -62,6 +62,7 @@ function mockCallArg<T>(
6262type FeishuDriveTool = {
6363 execute : ( callId : string , input : Record < string , unknown > ) => Promise < { details ?: unknown } > ;
6464 name ?: string ;
65+ parameters ?: unknown ;
6566} ;
6667
6768type FeishuDriveToolFactory = ( context : {
@@ -73,6 +74,26 @@ function firstToolFactory(mock: { mock: { calls: unknown[][] } }): FeishuDriveTo
7374 return mockCallArg < FeishuDriveToolFactory > ( mock , 0 , 0 ) ;
7475}
7576
77+ function buildDriveTool ( ) : FeishuDriveTool {
78+ const registerTool = vi . fn ( ) ;
79+ registerFeishuDriveTools (
80+ createDriveToolApi ( {
81+ config : {
82+ channels : {
83+ feishu : {
84+ enabled : true ,
85+ appId : "app_id" ,
86+ appSecret : "app_secret" , // pragma: allowlist secret
87+ tools : { drive : true } ,
88+ } ,
89+ } ,
90+ } ,
91+ registerTool,
92+ } ) ,
93+ ) ;
94+ return firstToolFactory ( registerTool ) ( { agentAccountId : undefined } ) ;
95+ }
96+
7697function firstLogMessage ( mock : { mock : { calls : unknown [ ] [ ] } } ) : string {
7798 return String ( mockCallArg < unknown > ( mock , 0 , 0 ) ) ;
7899}
@@ -107,6 +128,25 @@ function expectRequestCall(
107128 }
108129}
109130
131+ function schemaForAction (
132+ schema : unknown ,
133+ action : string ,
134+ ) : { properties ?: Record < string , unknown > } {
135+ const variants =
136+ ( schema as { anyOf ?: unknown [ ] ; oneOf ?: unknown [ ] } ) . anyOf ??
137+ ( schema as { anyOf ?: unknown [ ] ; oneOf ?: unknown [ ] } ) . oneOf ??
138+ [ ] ;
139+ const variant = variants . find ( ( entry ) => {
140+ const actionSchema = ( entry as { properties ?: { action ?: { const ?: unknown } } } ) . properties
141+ ?. action ;
142+ return actionSchema ?. const === action ;
143+ } ) ;
144+ if ( ! variant ) {
145+ throw new Error ( `Missing schema variant for action ${ action } ` ) ;
146+ }
147+ return variant as { properties ?: Record < string , unknown > } ;
148+ }
149+
110150describe ( "registerFeishuDriveTools" , ( ) => {
111151 const requestMock = vi . fn ( ) ;
112152
@@ -360,6 +400,91 @@ describe("registerFeishuDriveTools", () => {
360400 expect ( ( replyCommentResult . details as { reply_id ?: string } ) . reply_id ) . toBe ( "r4" ) ;
361401 } ) ;
362402
403+ it ( "lists a folder continuation page when page_token is provided" , async ( ) => {
404+ const listFiles = vi . fn ( ) . mockResolvedValue ( {
405+ code : 0 ,
406+ data : {
407+ files : [ { token : "file_1" , name : "File 1" , type : "docx" , url : "https://example.test/doc" } ] ,
408+ next_page_token : "page-3" ,
409+ } ,
410+ } ) ;
411+ createFeishuToolClientMock . mockReturnValue ( {
412+ drive : { file : { list : listFiles } } ,
413+ } ) ;
414+ const tool = buildDriveTool ( ) ;
415+ const listSchema = schemaForAction ( tool . parameters , "list" ) ;
416+ expect ( listSchema . properties ?. page_token ) . toMatchObject ( { type : "string" } ) ;
417+ expect ( listSchema . properties ?. page_size ) . toMatchObject ( {
418+ type : "integer" ,
419+ minimum : 1 ,
420+ maximum : 200 ,
421+ } ) ;
422+
423+ const result = await tool . execute ( "call-list-page-2" , {
424+ action : "list" ,
425+ folder_token : "folder_1" ,
426+ page_size : 25 ,
427+ page_token : "page-2" ,
428+ } ) ;
429+
430+ expect ( listFiles ) . toHaveBeenCalledWith ( {
431+ params : { folder_token : "folder_1" , page_size : 25 , page_token : "page-2" } ,
432+ } ) ;
433+ expect ( result . details ) . toMatchObject ( {
434+ files : [ { token : "file_1" , name : "File 1" , type : "docx" , url : "https://example.test/doc" } ] ,
435+ next_page_token : "page-3" ,
436+ } ) ;
437+ } ) ;
438+
439+ it ( "normalizes folder pagination and suppresses it for root listings" , async ( ) => {
440+ const listFiles = vi . fn ( ) . mockResolvedValue ( { code : 0 , data : { files : [ ] } } ) ;
441+ createFeishuToolClientMock . mockReturnValue ( { drive : { file : { list : listFiles } } } ) ;
442+ const tool = buildDriveTool ( ) ;
443+
444+ await tool . execute ( "call-list-normalized" , {
445+ action : "list" ,
446+ folder_token : " folder_1 " ,
447+ page_size : "25" ,
448+ page_token : " " ,
449+ } ) ;
450+ for ( const [ callId , folderToken ] of [
451+ [ "call-list-root" , undefined ] ,
452+ [ "call-list-empty" , " " ] ,
453+ [ "call-list-zero" , " 0 " ] ,
454+ ] as const ) {
455+ await tool . execute ( callId , {
456+ action : "list" ,
457+ folder_token : folderToken ,
458+ page_size : 25 ,
459+ page_token : "page-2" ,
460+ } ) ;
461+ }
462+
463+ expect ( listFiles ) . toHaveBeenNthCalledWith ( 1 , {
464+ params : { folder_token : "folder_1" , page_size : 25 } ,
465+ } ) ;
466+ for ( const callIndex of [ 2 , 3 , 4 ] ) {
467+ expect ( listFiles ) . toHaveBeenNthCalledWith ( callIndex , { params : { } } ) ;
468+ }
469+ } ) ;
470+
471+ it . each ( [ 0 , 201 , 1.5 ] ) ( "rejects invalid folder page_size %s" , async ( pageSize ) => {
472+ const listFiles = vi . fn ( ) ;
473+ createFeishuToolClientMock . mockReturnValue ( { drive : { file : { list : listFiles } } } ) ;
474+ const tool = buildDriveTool ( ) ;
475+
476+ const result = await tool . execute ( "call-list-invalid-page-size" , {
477+ action : "list" ,
478+ folder_token : "folder_1" ,
479+ page_size : pageSize ,
480+ } ) ;
481+
482+ expect ( result . details ) . toMatchObject ( {
483+ error : "page_size must be a positive integer between 1 and 200" ,
484+ } ) ;
485+ expect ( listFiles ) . not . toHaveBeenCalled ( ) ;
486+ } ) ;
487+
363488 it ( "defaults add_comment file_type to docx when omitted" , async ( ) => {
364489 const registerTool = vi . fn ( ) ;
365490 const infoSpy = vi . spyOn ( console , "info" ) . mockImplementation ( ( ) => { } ) ;
0 commit comments