@@ -47,6 +47,7 @@ const {
4747 countPendingSeatInvitations,
4848 resolveSeatCapacity,
4949 collectWorkspaceCredentialSummary,
50+ getSourceOrganization,
5051} = vi . hoisted ( ( ) => ( {
5152 resolveMoveEntitlements : vi . fn ( ( ) =>
5253 Promise . resolve ( {
@@ -75,8 +76,17 @@ const {
7576 countPendingSeatInvitations : vi . fn ( ( ) => Promise . resolve ( 0 ) ) ,
7677 resolveSeatCapacity : vi . fn ( ( ) => Promise . resolve ( 10 ) ) ,
7778 collectWorkspaceCredentialSummary : vi . fn ( ) ,
79+ getSourceOrganization : vi . fn ( ) ,
7880} ) )
7981
82+ const SOURCE_ORGANIZATION = {
83+ id : 'org-source' ,
84+ name : 'Source' ,
85+ ownerId : 'source-owner' ,
86+ ownerName : 'Source Owner' ,
87+ ownerEmail : 'source-owner@example.com' ,
88+ }
89+
8090const EMPTY_CREDENTIALS = {
8191 items : [ ] as Array < {
8292 id : string
@@ -101,6 +111,13 @@ const POPULATED_CREDENTIALS = {
101111 byokKeyCount : 2 ,
102112}
103113
114+ /** A workspace whose secrets exceed the response bounds, so rows were dropped. */
115+ const TRUNCATED_CREDENTIALS = {
116+ ...POPULATED_CREDENTIALS ,
117+ truncatedCredentials : 3 ,
118+ truncatedEnvironmentVariableKeys : 7 ,
119+ }
120+
104121vi . mock ( '@sim/audit' , ( ) => ( {
105122 AuditAction : {
106123 WORKSPACE_UPDATED : 'workspace.updated' ,
@@ -157,15 +174,7 @@ vi.mock('@/lib/workspaces/admin-move-source-impact', () => ({
157174 findRetainedCollaboratorCaps : vi . fn ( ( ) => Promise . resolve ( [ ] ) ) ,
158175 findUnpublishableCustomBlocks,
159176 findSourceOrgCustomBlocksForWorkspace,
160- getSourceOrganization : vi . fn ( ( ) =>
161- Promise . resolve ( {
162- id : 'org-source' ,
163- name : 'Source' ,
164- ownerId : 'source-owner' ,
165- ownerName : 'Source Owner' ,
166- ownerEmail : 'source-owner@example.com' ,
167- } )
168- ) ,
177+ getSourceOrganization,
169178 resolveMoveEntitlements,
170179 willBrandingChange : vi . fn ( ( ) => Promise . resolve ( false ) ) ,
171180} ) )
@@ -269,6 +278,7 @@ beforeEach(() => {
269278 capabilitiesLost : [ ] ,
270279 } )
271280 collectWorkspaceCredentialSummary . mockResolvedValue ( EMPTY_CREDENTIALS )
281+ getSourceOrganization . mockResolvedValue ( SOURCE_ORGANIZATION )
272282 changeWorkspaceStoragePayerInTx . mockResolvedValue ( {
273283 billableBytes : 128 ,
274284 newPayer : { type : 'organization' , id : destination . id } ,
@@ -898,6 +908,33 @@ describe('moveWorkspaceToOrganization retries', () => {
898908 expect ( view . credentials ) . toEqual ( POPULATED_CREDENTIALS )
899909 } )
900910
911+ /**
912+ * A recorded id whose organization has since been deleted is the third state:
913+ * the payload answered, but the answer can no longer be resolved to a name.
914+ */
915+ it ( 'distinguishes a deleted source organization from an unrecorded one' , async ( ) => {
916+ getSourceOrganization . mockResolvedValueOnce ( null )
917+ queueMoveOperationSelects ( {
918+ actor : { id : null , name : 'Admin Panel' , email : 'admin@sim.ai' } ,
919+ previousBillingOwnerId : personalWorkspace . billedAccountUserId ,
920+ newBillingOwnerId : destination . ownerId ,
921+ organizationAssignedAt : '2026-08-20T00:00:00.000Z' ,
922+ sourceOrganizationId : 'org-source' ,
923+ } )
924+
925+ const view = await getWorkspaceMoveOperation (
926+ movedWorkspace . id ,
927+ destination . id ,
928+ movedWorkspace . ownerId ,
929+ 'operation-1'
930+ )
931+
932+ expect ( view . sourceOrganization ) . toBeNull ( )
933+ expect ( view . notices ) . toEqual ( [
934+ 'The organization this workspace came from has since been deleted, so it can no longer be named.' ,
935+ ] )
936+ } )
937+
901938 it ( 'reports the workspace credentials in the applied summary' , async ( ) => {
902939 queueMoveSelects ( organizationWorkspace )
903940 collectWorkspaceCredentialSummary . mockResolvedValueOnce ( POPULATED_CREDENTIALS )
@@ -916,6 +953,52 @@ describe('moveWorkspaceToOrganization retries', () => {
916953 expect . anything ( )
917954 )
918955 expect ( summary . credentials ) . toEqual ( POPULATED_CREDENTIALS )
956+ /** Nothing was dropped, so the review is complete and says nothing about truncation. */
957+ expect ( summary . sourceOrganizationImpact . truncated ) . toBeNull ( )
958+ } )
959+
960+ /**
961+ * The applied path used to hardcode these two counters to zero, which would
962+ * present a truncated credential list as a complete one.
963+ */
964+ it ( 'carries dropped credential counts into the applied truncation record' , async ( ) => {
965+ queueMoveSelects ( organizationWorkspace )
966+ collectWorkspaceCredentialSummary . mockResolvedValueOnce ( TRUNCATED_CREDENTIALS )
967+
968+ const summary = await moveWorkspaceToOrganization ( {
969+ workspaceId : organizationWorkspace . id ,
970+ destinationOrganizationId : destination . id ,
971+ adminEmail : 'admin@sim.ai' ,
972+ durableOperationId : 'operation-1' ,
973+ } )
974+
975+ expect ( summary . sourceOrganizationImpact . truncated ) . toMatchObject ( {
976+ credentials : 3 ,
977+ environmentVariableKeys : 7 ,
978+ } )
979+ } )
980+
981+ it ( 'carries dropped credential counts into a reloaded truncation record' , async ( ) => {
982+ collectWorkspaceCredentialSummary . mockResolvedValueOnce ( TRUNCATED_CREDENTIALS )
983+ queueMoveOperationSelects ( {
984+ actor : { id : null , name : 'Admin Panel' , email : 'admin@sim.ai' } ,
985+ previousBillingOwnerId : personalWorkspace . billedAccountUserId ,
986+ newBillingOwnerId : destination . ownerId ,
987+ organizationAssignedAt : '2026-08-20T00:00:00.000Z' ,
988+ sourceOrganizationId : 'org-source' ,
989+ } )
990+
991+ const view = await getWorkspaceMoveOperation (
992+ movedWorkspace . id ,
993+ destination . id ,
994+ movedWorkspace . ownerId ,
995+ 'operation-1'
996+ )
997+
998+ expect ( view . sourceOrganizationImpact . truncated ) . toMatchObject ( {
999+ credentials : 3 ,
1000+ environmentVariableKeys : 7 ,
1001+ } )
9191002 } )
9201003
9211004 it ( 'reports the workspace credentials on a retry of a completed move' , async ( ) => {
0 commit comments