@@ -12,6 +12,9 @@ const {
1212 mockBatchInsertRows,
1313 mockReplaceTableRows,
1414 mockAddWorkflowGroup,
15+ mockCreateTable,
16+ mockDeleteTable,
17+ mockGetWorkspaceTableLimits,
1518 fakeEnrichment,
1619} = vi . hoisted ( ( ) => ( {
1720 mockResolveWorkspaceFileReference : vi . fn ( ) ,
@@ -20,6 +23,9 @@ const {
2023 mockBatchInsertRows : vi . fn ( ) ,
2124 mockReplaceTableRows : vi . fn ( ) ,
2225 mockAddWorkflowGroup : vi . fn ( ) ,
26+ mockCreateTable : vi . fn ( ) ,
27+ mockDeleteTable : vi . fn ( ) ,
28+ mockGetWorkspaceTableLimits : vi . fn ( ) ,
2329 fakeEnrichment : {
2430 id : 'work-email' ,
2531 name : 'Work Email' ,
@@ -54,13 +60,13 @@ vi.mock('@/lib/table/service', () => ({
5460 addWorkflowGroup : mockAddWorkflowGroup ,
5561 batchInsertRows : mockBatchInsertRows ,
5662 batchUpdateRows : vi . fn ( ) ,
57- createTable : vi . fn ( ) ,
63+ createTable : mockCreateTable ,
5864 deleteColumn : vi . fn ( ) ,
5965 deleteColumns : vi . fn ( ) ,
6066 deleteRow : vi . fn ( ) ,
6167 deleteRowsByFilter : vi . fn ( ) ,
6268 deleteRowsByIds : vi . fn ( ) ,
63- deleteTable : vi . fn ( ) ,
69+ deleteTable : mockDeleteTable ,
6470 getRowById : vi . fn ( ) ,
6571 getTableById : mockGetTableById ,
6672 insertRow : vi . fn ( ) ,
@@ -74,6 +80,10 @@ vi.mock('@/lib/table/service', () => ({
7480 updateRowsByFilter : vi . fn ( ) ,
7581} ) )
7682
83+ vi . mock ( '@/lib/table/billing' , ( ) => ( {
84+ getWorkspaceTableLimits : mockGetWorkspaceTableLimits ,
85+ } ) )
86+
7787import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
7888
7989function buildTable ( overrides : Partial < TableDefinition > = { } ) : TableDefinition {
@@ -232,6 +242,86 @@ describe('userTableServerTool.import_file', () => {
232242 } )
233243} )
234244
245+ describe ( 'userTableServerTool.create_from_file' , ( ) => {
246+ beforeEach ( ( ) => {
247+ vi . clearAllMocks ( )
248+ mockResolveWorkspaceFileReference . mockResolvedValue ( { name : 'people.csv' , type : 'text/csv' } )
249+ mockDownloadWorkspaceFile . mockResolvedValue ( Buffer . from ( 'name,age\nAlice,30\nBob,40' ) )
250+ mockGetWorkspaceTableLimits . mockResolvedValue ( { maxRowsPerTable : 1000 , maxTables : 3 } )
251+ mockCreateTable . mockResolvedValue ( buildTable ( { id : 'tbl_new' , name : 'people' } ) )
252+ mockBatchInsertRows . mockImplementation ( async ( data : { rows : unknown [ ] } ) =>
253+ data . rows . map ( ( _ , i ) => ( { id : `row_${ i } ` } ) )
254+ )
255+ } )
256+
257+ it ( 'stamps the workspace plan limits on the created table' , async ( ) => {
258+ const result = await userTableServerTool . execute (
259+ { operation : 'create_from_file' , args : { fileId : 'file-1' } } ,
260+ { userId : 'user-1' , workspaceId : 'workspace-1' }
261+ )
262+
263+ expect ( result . success ) . toBe ( true )
264+ expect ( mockGetWorkspaceTableLimits ) . toHaveBeenCalledWith ( 'workspace-1' )
265+ expect ( mockCreateTable ) . toHaveBeenCalledTimes ( 1 )
266+ const createArgs = mockCreateTable . mock . calls [ 0 ] [ 0 ] as { maxRows : number ; maxTables : number }
267+ expect ( createArgs . maxRows ) . toBe ( 1000 )
268+ expect ( createArgs . maxTables ) . toBe ( 3 )
269+ } )
270+
271+ it ( 'rejects a file exceeding the plan row limit without creating a table' , async ( ) => {
272+ mockGetWorkspaceTableLimits . mockResolvedValueOnce ( { maxRowsPerTable : 1 , maxTables : 3 } )
273+
274+ const result = await userTableServerTool . execute (
275+ { operation : 'create_from_file' , args : { fileId : 'file-1' } } ,
276+ { userId : 'user-1' , workspaceId : 'workspace-1' }
277+ )
278+
279+ expect ( result . success ) . toBe ( false )
280+ expect ( result . message ) . toMatch ( / e x c e e d s t h i s p l a n ' s l i m i t / i)
281+ expect ( mockCreateTable ) . not . toHaveBeenCalled ( )
282+ expect ( mockDeleteTable ) . not . toHaveBeenCalled ( )
283+ } )
284+
285+ it ( 'deletes the created table when row insertion fails' , async ( ) => {
286+ mockBatchInsertRows . mockRejectedValueOnce ( new Error ( 'Maximum row limit (1000) reached' ) )
287+
288+ const result = await userTableServerTool . execute (
289+ { operation : 'create_from_file' , args : { fileId : 'file-1' } } ,
290+ { userId : 'user-1' , workspaceId : 'workspace-1' }
291+ )
292+
293+ expect ( result . success ) . toBe ( false )
294+ expect ( mockDeleteTable ) . toHaveBeenCalledWith ( 'tbl_new' , expect . any ( String ) )
295+ } )
296+ } )
297+
298+ describe ( 'userTableServerTool.create' , ( ) => {
299+ beforeEach ( ( ) => {
300+ vi . clearAllMocks ( )
301+ mockGetWorkspaceTableLimits . mockResolvedValue ( { maxRowsPerTable : 1000 , maxTables : 3 } )
302+ mockCreateTable . mockResolvedValue ( buildTable ( { id : 'tbl_new' , name : 'People' } ) )
303+ } )
304+
305+ it ( 'stamps the workspace plan limits on the created table' , async ( ) => {
306+ const result = await userTableServerTool . execute (
307+ {
308+ operation : 'create' ,
309+ args : {
310+ name : 'People' ,
311+ schema : { columns : [ { name : 'name' , type : 'string' , required : true } ] } ,
312+ } ,
313+ } ,
314+ { userId : 'user-1' , workspaceId : 'workspace-1' }
315+ )
316+
317+ expect ( result . success ) . toBe ( true )
318+ expect ( mockGetWorkspaceTableLimits ) . toHaveBeenCalledWith ( 'workspace-1' )
319+ const createArgs = mockCreateTable . mock . calls [ 0 ] [ 0 ] as { maxRows : number ; maxTables : number }
320+ expect ( createArgs . maxRows ) . toBe ( 1000 )
321+ expect ( createArgs . maxTables ) . toBe ( 3 )
322+ } )
323+ } )
324+
235325describe ( 'userTableServerTool.list_enrichments' , ( ) => {
236326 beforeEach ( ( ) => {
237327 vi . clearAllMocks ( )
0 commit comments