@@ -16,6 +16,8 @@ const mocks = vi.hoisted(() => ({
1616 resolvePermission : vi . fn ( ) ,
1717 resolveWorkspaceContext : vi . fn ( ) ,
1818 signal : vi . fn ( ) ,
19+ validateMapping : vi . fn ( ) ,
20+ CsvImportValidationError : class extends Error { } ,
1921} ) )
2022
2123vi . mock ( '@sim/audit' , ( ) => ( {
@@ -39,14 +41,11 @@ vi.mock('@/lib/table', () => ({
3941 batchInsertRows : mocks . batchInsert ,
4042 buildAutoMapping : vi . fn ( ( ) => ( { name : 'name' } ) ) ,
4143 coerceRowsForTable : ( rows : unknown [ ] ) => rows ,
44+ CsvImportValidationError : mocks . CsvImportValidationError ,
4245 CSV_MAX_BATCH_SIZE : 1000 ,
4346 getWorkspaceTableLimits : vi . fn ( ( ) => ( { maxRowsPerTable : 100 , maxTables : 5 } ) ) ,
4447 replaceTableRows : vi . fn ( ) ,
45- validateMapping : vi . fn ( ( ) => ( {
46- effectiveMap : new Map ( [ [ 'name' , 'name' ] ] ) ,
47- mappedHeaders : [ 'name' ] ,
48- skippedHeaders : [ ] ,
49- } ) ) ,
48+ validateMapping : mocks . validateMapping ,
5049} ) )
5150vi . mock ( '@/lib/table/application/context' , ( ) => ( {
5251 resolveActiveTableContext : mocks . resolveTableContext ,
@@ -94,6 +93,7 @@ const principal = {
9493 audience : 'sim:tables' ,
9594 issuedAt : new Date ( '2026-08-01T00:00:00.000Z' ) ,
9695 expiresAt : new Date ( '2099-08-01T00:00:00.000Z' ) ,
96+ resourceScope : { tableId : 'table-1' } ,
9797}
9898const input = {
9999 kind : 'inline' as const ,
@@ -136,6 +136,11 @@ describe('Copilot workspace-file table creation', () => {
136136 mocks . batchInsert . mockResolvedValue ( [ { id : 'row-1' } ] )
137137 mocks . markJob . mockResolvedValue ( true )
138138 mocks . releaseJob . mockResolvedValue ( true )
139+ mocks . validateMapping . mockReturnValue ( {
140+ effectiveMap : new Map ( [ [ 'name' , 'name' ] ] ) ,
141+ mappedHeaders : [ 'name' ] ,
142+ skippedHeaders : [ ] ,
143+ } )
139144 } )
140145
141146 it ( 'owns table creation, row insertion, audit, and shared effects' , async ( ) => {
@@ -201,6 +206,53 @@ describe('Copilot workspace-file table creation', () => {
201206 expect ( events ) . toEqual ( [ 'claim' , 'load' , 'mutate' , 'release' ] )
202207 } )
203208
209+ it ( 'checks for a user stop after loading and before every inline insert batch' , async ( ) => {
210+ const assertNotAborted = vi . fn ( )
211+
212+ await importWorkspaceFileIntoTable . execute ( {
213+ principal,
214+ input : {
215+ kind : 'inline' ,
216+ tableId : 'table-1' ,
217+ assertedWorkspaceId : 'workspace-1' ,
218+ sourceFile : input . sourceFile ,
219+ mode : 'append' ,
220+ assertNotAborted,
221+ loadRows : async ( ) => ( {
222+ headers : [ 'name' ] ,
223+ rows : Array . from ( { length : 1001 } , ( _ , index ) => ( { name : `Person ${ index } ` } ) ) ,
224+ } ) ,
225+ } ,
226+ } )
227+
228+ expect ( assertNotAborted ) . toHaveBeenCalledTimes ( 3 )
229+ expect ( mocks . batchInsert ) . toHaveBeenCalledTimes ( 2 )
230+ } )
231+
232+ it ( 'classifies mapping failures before mutation so Copilot can correct them' , async ( ) => {
233+ mocks . validateMapping . mockImplementationOnce ( ( ) => {
234+ throw new mocks . CsvImportValidationError ( 'Mapping references an unknown column' )
235+ } )
236+
237+ await expect (
238+ importWorkspaceFileIntoTable . execute ( {
239+ principal,
240+ input : {
241+ kind : 'inline' ,
242+ tableId : 'table-1' ,
243+ assertedWorkspaceId : 'workspace-1' ,
244+ sourceFile : input . sourceFile ,
245+ mode : 'append' ,
246+ loadRows : async ( ) => ( { headers : [ 'name' ] , rows : [ { name : 'Ada' } ] } ) ,
247+ } ,
248+ } )
249+ ) . rejects . toMatchObject ( {
250+ code : 'validation' ,
251+ message : 'Mapping references an unknown column' ,
252+ } )
253+ expect ( mocks . batchInsert ) . not . toHaveBeenCalled ( )
254+ } )
255+
204256 it ( 'rolls back and propagates unknown insertion failures without audit or effects' , async ( ) => {
205257 const failure = new Error ( 'database unavailable' )
206258 mocks . batchInsert . mockRejectedValueOnce ( failure )
0 commit comments