11import * as fs from 'fs-extra' ;
22import archiver from 'archiver' ;
3- import { pathManager , stateManager } from '@aws-amplify/amplify-cli-core' ;
3+ import { AmplifyFault , pathManager , stateManager } from '@aws-amplify/amplify-cli-core' ;
44import { Redactor } from '@aws-amplify/amplify-cli-logger' ;
55import { WriteStream } from 'fs-extra' ;
66import fetch from 'node-fetch' ;
@@ -10,7 +10,20 @@ import { run } from '../../commands/diagnose';
1010import { Context } from '../../domain/context' ;
1111
1212jest . mock ( 'uuid' ) ;
13- jest . mock ( '@aws-amplify/amplify-cli-core' ) ;
13+ jest . mock ( '@aws-amplify/amplify-cli-core' , ( ) => {
14+ const actual = jest . requireActual ( '@aws-amplify/amplify-cli-core' ) ;
15+ return {
16+ ...actual ,
17+ pathManager : { findProjectRoot : jest . fn ( ) } ,
18+ stateManager : {
19+ getBackendConfig : jest . fn ( ) ,
20+ getProjectConfig : jest . fn ( ) ,
21+ getMeta : jest . fn ( ) . mockReturnValue ( { } ) ,
22+ getCurrentEnvName : jest . fn ( ) . mockReturnValue ( 'dev' ) ,
23+ } ,
24+ spinner : { start : jest . fn ( ) , stop : jest . fn ( ) , succeed : jest . fn ( ) , fail : jest . fn ( ) } ,
25+ } ;
26+ } ) ;
1427jest . mock ( '../../commands/helpers/collect-files' ) ;
1528jest . mock ( '../../commands/helpers/encryption-helpers' , ( ) => ( {
1629 createHashedIdentifier : jest . fn ( ) . mockReturnValue ( {
@@ -163,4 +176,67 @@ describe('run report command', () => {
163176 expect ( fetch ) . toBeCalled ( ) ;
164177 expect ( zipperMock . append ) . toBeCalledTimes ( 3 ) ;
165178 } ) ;
179+
180+ it ( 'serializes errors with circular downstream references without throwing' , async ( ) => {
181+ const contextMock = {
182+ usageData : {
183+ getUsageDataPayload : jest . fn ( ) . mockReturnValue ( {
184+ sessionUuid : 'sessionId' ,
185+ installationUuid : '' ,
186+ } ) ,
187+ } ,
188+ exeInfo : { } ,
189+ input : {
190+ options : {
191+ 'send-report' : true ,
192+ } ,
193+ } ,
194+ } ;
195+
196+ const pathManagerMock = pathManager as jest . Mocked < typeof pathManager > ;
197+ pathManagerMock . findProjectRoot = jest . fn ( ) . mockReturnValue ( 'user/source/myProject' ) ;
198+ const stateManagerMock = stateManager as jest . Mocked < typeof stateManager > ;
199+ stateManagerMock . getBackendConfig = jest . fn ( ) . mockReturnValue ( mockMeta ) ;
200+ stateManagerMock . getProjectConfig = jest . fn ( ) . mockReturnValue ( { projectName : 'myProject' } ) ;
201+
202+ const collectFilesMock = collectFiles as jest . MockedFunction < typeof collectFiles > ;
203+ collectFilesMock . mockReturnValue ( [ ] ) ;
204+
205+ const mockArchiver = archiver as jest . Mocked < typeof archiver > ;
206+ const zipperMock = {
207+ append : jest . fn ( ) ,
208+ pipe : jest . fn ( ) ,
209+ finalize : jest . fn ( ) ,
210+ } ;
211+ mockArchiver . create = jest . fn ( ) . mockReturnValue ( zipperMock ) ;
212+
213+ const fsMock = fs as jest . Mocked < typeof fs > ;
214+ fsMock . createWriteStream . mockReturnValue ( {
215+ on : jest . fn ( ) . mockImplementation ( ( event , resolveFunction ) => {
216+ if ( event === 'close' ) {
217+ resolveFunction ( ) ;
218+ }
219+ } ) ,
220+ error : jest . fn ( ) ,
221+ } as unknown as WriteStream ) ;
222+
223+ // Construct a downstream error with a self-referential property, mirroring the
224+ // `$response.req <-> $response.res` cycle that AWS SDK v3 ServiceException carries.
225+ const downstream = new Error ( 'underlying AWS error' ) as Error & { $response ?: unknown } ;
226+ const response : { req : unknown } = { req : { } } ;
227+ const request : { res : unknown } = { res : response } ;
228+ response . req = request ;
229+ downstream . $response = response ;
230+
231+ const fault = new AmplifyFault ( 'NotificationsChannelSmsFault' , { message : 'Failed to enable the SMS channel.' } , downstream ) ;
232+
233+ const contextMockTyped = contextMock as unknown as Context ;
234+ await expect ( run ( contextMockTyped , fault ) ) . resolves . not . toThrow ( ) ;
235+
236+ const errorJsonCall = zipperMock . append . mock . calls . find ( ( [ , meta ] : [ unknown , { name : string } ] ) => meta ?. name === 'error.json' ) ;
237+ expect ( errorJsonCall ) . toBeDefined ( ) ;
238+ const payload = JSON . parse ( errorJsonCall ! [ 0 ] as string ) ;
239+ expect ( payload . error . name ) . toBe ( 'NotificationsChannelSmsFault' ) ;
240+ expect ( payload . downstreamException . message ) . toBe ( 'underlying AWS error' ) ;
241+ } ) ;
166242} ) ;
0 commit comments