@@ -86,6 +86,7 @@ function payload(workspaceIds: string[]) {
8686 memberId : null ,
8787 transferredFromOrganizationId : null ,
8888 nextWorkspaceIndex : 0 ,
89+ currentWorkspaceId : null ,
8990 } ,
9091 }
9192}
@@ -187,6 +188,7 @@ describe('durable admin member operation', () => {
187188 memberId : 'member-new' ,
188189 transferredFromOrganizationId : 'org-old' ,
189190 nextWorkspaceIndex : 2 ,
191+ currentWorkspaceId : null ,
190192 } ,
191193 } )
192194 } )
@@ -214,6 +216,92 @@ describe('durable admin member operation', () => {
214216 memberId : 'member-new' ,
215217 transferredFromOrganizationId : 'org-old' ,
216218 nextWorkspaceIndex : 10 ,
219+ currentWorkspaceId : null ,
220+ } ,
221+ } )
222+ } )
223+
224+ it ( 'checkpoints the active workspace before attempting its move' , async ( ) => {
225+ queueTableRows ( member , [ { id : 'member-new' , role : 'member' , organizationId : 'org-new' } ] )
226+ mocks . moveWorkspace . mockRejectedValueOnce ( new Error ( 'Move failed' ) )
227+ const checkpointPayload = vi . fn ( )
228+
229+ await expect (
230+ processAdminMemberOperation ( payload ( [ 'workspace-1' ] ) , {
231+ eventId : 'operation-1' ,
232+ eventType : 'admin.organization-member-operation' ,
233+ attempts : 0 ,
234+ checkpointPayload,
235+ } )
236+ ) . rejects . toThrow ( 'Move failed' )
237+
238+ expect ( checkpointPayload ) . toHaveBeenLastCalledWith ( {
239+ progress : {
240+ memberId : 'member-new' ,
241+ transferredFromOrganizationId : 'org-old' ,
242+ nextWorkspaceIndex : 0 ,
243+ currentWorkspaceId : 'workspace-1' ,
244+ } ,
245+ } )
246+ } )
247+
248+ it ( 'does not mislabel a membership failure as a workspace failure' , async ( ) => {
249+ queueTableRows ( outboxEvent , [
250+ {
251+ id : '1c38ca61-79d5-4d24-8094-c29cb52132ba' ,
252+ eventType : 'admin.organization-member-operation' ,
253+ payload : payload ( [ 'workspace-1' , 'workspace-2' ] ) ,
254+ status : 'dead_letter' ,
255+ lastError : 'Seat limit reached' ,
256+ createdAt : new Date ( '2026-08-20T00:00:00.000Z' ) ,
257+ } ,
258+ ] )
259+ queueTableRows ( outboxEvent , [ { selected : 0 , completed : 0 , failed : 0 } ] )
260+
261+ await expect (
262+ getAdminMemberOperation ( 'org-new' , '1c38ca61-79d5-4d24-8094-c29cb52132ba' )
263+ ) . resolves . toMatchObject ( {
264+ error : 'Seat limit reached' ,
265+ workspaceMoves : {
266+ selected : 2 ,
267+ moved : 0 ,
268+ pending : 2 ,
269+ failedCount : 0 ,
270+ failed : [ ] ,
271+ } ,
272+ } )
273+ } )
274+
275+ it ( 'separates the active failed workspace from still-pending workspaces' , async ( ) => {
276+ queueTableRows ( outboxEvent , [
277+ {
278+ id : '1c38ca61-79d5-4d24-8094-c29cb52132ba' ,
279+ eventType : 'admin.organization-member-operation' ,
280+ payload : {
281+ ...payload ( [ 'workspace-1' , 'workspace-2' , 'workspace-3' ] ) ,
282+ progress : {
283+ memberId : 'member-new' ,
284+ transferredFromOrganizationId : 'org-old' ,
285+ nextWorkspaceIndex : 1 ,
286+ currentWorkspaceId : 'workspace-2' ,
287+ } ,
288+ } ,
289+ status : 'dead_letter' ,
290+ lastError : 'Move failed' ,
291+ createdAt : new Date ( '2026-08-20T00:00:00.000Z' ) ,
292+ } ,
293+ ] )
294+ queueTableRows ( outboxEvent , [ { selected : 0 , completed : 0 , failed : 0 } ] )
295+
296+ await expect (
297+ getAdminMemberOperation ( 'org-new' , '1c38ca61-79d5-4d24-8094-c29cb52132ba' )
298+ ) . resolves . toMatchObject ( {
299+ workspaceMoves : {
300+ selected : 3 ,
301+ moved : 1 ,
302+ pending : 1 ,
303+ failedCount : 1 ,
304+ failed : [ { workspaceId : 'workspace-2' , error : 'Move failed' } ] ,
217305 } ,
218306 } )
219307 } )
@@ -229,6 +317,7 @@ describe('durable admin member operation', () => {
229317 memberId : 'member-new' ,
230318 transferredFromOrganizationId : 'org-old' ,
231319 nextWorkspaceIndex : 1 ,
320+ currentWorkspaceId : null ,
232321 } ,
233322 } ,
234323 status : 'completed' ,
0 commit comments