@@ -11,7 +11,10 @@ const state = {
1111 _cmd : string ,
1212 _args ?: string [ ] ,
1313 ) : Promise < number > => 0 ,
14- getExecOutputImpl : async ( ) : Promise < {
14+ getExecOutputImpl : async (
15+ _cmd : string ,
16+ _args ?: string [ ] ,
17+ ) : Promise < {
1518 stdout : string ;
1619 stderr : string ;
1720 exitCode : number ;
@@ -64,7 +67,16 @@ vi.mock("@actions/exec", () => ({
6467 return state . execImpl ( cmd , args ) ;
6568 } ,
6669 ) ,
67- getExecOutput : vi . fn ( async ( ) => state . getExecOutputImpl ( ) ) ,
70+ getExecOutput : vi . fn (
71+ async (
72+ cmd : string ,
73+ args ?: string [ ] ,
74+ opts ?: unknown ,
75+ ) => {
76+ state . execCalls . push ( [ cmd , args , opts ] ) ;
77+ return state . getExecOutputImpl ( cmd , args ) ;
78+ } ,
79+ ) ,
6880} ) ) ;
6981
7082vi . mock ( "@actions/io" , ( ) => ( {
@@ -105,7 +117,10 @@ function setupMocks({
105117
106118 // Setup exec implementations
107119 state . execImpl = async ( _cmd : string , _args ?: string [ ] ) => 0 ;
108- state . getExecOutputImpl = async ( ) => ( {
120+ state . getExecOutputImpl = async (
121+ _cmd : string ,
122+ _args ?: string [ ] ,
123+ ) => ( {
109124 stdout : hasChanges ? "M openapi/openapi.json\n" : "" ,
110125 stderr : "" ,
111126 exitCode : 0 ,
@@ -251,6 +266,7 @@ describe("updateFromSourceSpec", () => {
251266 it ( "should rebase and retry push when regular push fails" , async ( ) => {
252267 setupMocks ( { hasChanges : true , existingPRNumber : null } ) ;
253268 let pushAttempt = 0 ;
269+ // First push via exec throws (regular push)
254270 state . execImpl = async (
255271 cmd : string ,
256272 args ?: string [ ] ,
@@ -267,18 +283,24 @@ describe("updateFromSourceSpec", () => {
267283 }
268284 return 0 ;
269285 } ;
270- await importAndRun ( ) ;
271-
272- // Should have attempted push twice (initial + after rebase)
273- const pushCalls = state . execCalls . filter (
274- ( [ cmd , args ] ) =>
286+ // Rebase + second push via getExecOutput succeed
287+ state . getExecOutputImpl = async (
288+ cmd : string ,
289+ args ?: string [ ] ,
290+ ) => {
291+ // git status --porcelain returns changes
292+ if (
275293 cmd === "git" &&
276294 Array . isArray ( args ) &&
277- args . includes ( "push" ) ,
278- ) ;
279- expect ( pushCalls . length ) . toBe ( 2 ) ;
295+ args . includes ( "--porcelain" )
296+ ) {
297+ return { stdout : "M openapi/openapi.json\n" , stderr : "" , exitCode : 0 } ;
298+ }
299+ return { stdout : "" , stderr : "" , exitCode : 0 } ;
300+ } ;
301+ await importAndRun ( ) ;
280302
281- // Should have done a pull --rebase between pushes
303+ // Should have done a pull --rebase
282304 const rebasePull = state . execCalls . find (
283305 ( [ cmd , args ] ) =>
284306 cmd === "git" &&
@@ -294,20 +316,47 @@ describe("updateFromSourceSpec", () => {
294316
295317 it ( "should comment on PR when push and rebase both fail" , async ( ) => {
296318 setupMocks ( { hasChanges : true , existingPRNumber : 42 } ) ;
319+ // First push via exec throws
297320 state . execImpl = async (
298321 cmd : string ,
299322 args ?: string [ ] ,
300323 ) : Promise < number > => {
301324 if (
302325 cmd === "git" &&
303326 Array . isArray ( args ) &&
304- ( args . includes ( "push" ) ||
305- ( args . includes ( "pull" ) && args . includes ( "--rebase" ) ) )
327+ args . includes ( "push" )
306328 ) {
307- throw new Error ( "merge conflict " ) ;
329+ throw new Error ( "rejected (non-fast-forward) " ) ;
308330 }
309331 return 0 ;
310332 } ;
333+ // Rebase via getExecOutput fails with detailed error
334+ state . getExecOutputImpl = async (
335+ cmd : string ,
336+ args ?: string [ ] ,
337+ ) => {
338+ if (
339+ cmd === "git" &&
340+ Array . isArray ( args ) &&
341+ args . includes ( "--porcelain" )
342+ ) {
343+ return { stdout : "M openapi/openapi.json\n" , stderr : "" , exitCode : 0 } ;
344+ }
345+ if (
346+ cmd === "git" &&
347+ Array . isArray ( args ) &&
348+ args . includes ( "pull" ) &&
349+ args . includes ( "--rebase" )
350+ ) {
351+ return {
352+ stdout : "CONFLICT (content): Merge conflict in fern/openapi/openapi.json" ,
353+ stderr : "error: could not apply abc1234... Update API" ,
354+ exitCode : 1 ,
355+ } ;
356+ }
357+ // rebase --abort succeeds
358+ return { stdout : "" , stderr : "" , exitCode : 0 } ;
359+ } ;
311360 await importAndRun ( ) ;
312361
313362 // Should NOT create a new PR
@@ -322,12 +371,13 @@ describe("updateFromSourceSpec", () => {
322371 } ) ,
323372 ) ;
324373
325- // Comment body should mention sync failed and include error details
374+ // Comment body should mention sync failed and include detailed error
326375 const commentCall = mockIssuesCreateComment . mock . calls [ 0 ] [ 0 ] ;
327376 expect ( commentCall . body ) . toContain ( "Sync failed" ) ;
328377 expect ( commentCall . body ) . toContain ( "merge conflicts" ) ;
329378 expect ( commentCall . body ) . toContain ( "Rebase error:" ) ;
330- expect ( commentCall . body ) . toContain ( "merge conflict" ) ;
379+ expect ( commentCall . body ) . toContain ( "CONFLICT (content)" ) ;
380+ expect ( commentCall . body ) . toContain ( "could not apply" ) ;
331381
332382 // Action should still fail (not silently succeed)
333383 expect ( state . setFailedCalls ) . toEqual (
@@ -339,20 +389,42 @@ describe("updateFromSourceSpec", () => {
339389
340390 it ( "should call setFailed when push fails and no existing PR to comment on" , async ( ) => {
341391 setupMocks ( { hasChanges : true , existingPRNumber : null } ) ;
392+ // First push via exec throws
342393 state . execImpl = async (
343394 cmd : string ,
344395 args ?: string [ ] ,
345396 ) : Promise < number > => {
346397 if (
347398 cmd === "git" &&
348399 Array . isArray ( args ) &&
349- ( args . includes ( "push" ) ||
350- ( args . includes ( "pull" ) && args . includes ( "--rebase" ) ) )
400+ args . includes ( "push" )
351401 ) {
352- throw new Error ( "merge conflict " ) ;
402+ throw new Error ( "rejected (non-fast-forward) " ) ;
353403 }
354404 return 0 ;
355405 } ;
406+ // Rebase via getExecOutput also fails
407+ state . getExecOutputImpl = async (
408+ cmd : string ,
409+ args ?: string [ ] ,
410+ ) => {
411+ if (
412+ cmd === "git" &&
413+ Array . isArray ( args ) &&
414+ args . includes ( "--porcelain" )
415+ ) {
416+ return { stdout : "M openapi/openapi.json\n" , stderr : "" , exitCode : 0 } ;
417+ }
418+ if (
419+ cmd === "git" &&
420+ Array . isArray ( args ) &&
421+ args . includes ( "pull" ) &&
422+ args . includes ( "--rebase" )
423+ ) {
424+ return { stdout : "" , stderr : "merge conflict" , exitCode : 1 } ;
425+ }
426+ return { stdout : "" , stderr : "" , exitCode : 0 } ;
427+ } ;
356428 await importAndRun ( ) ;
357429
358430 expect ( mockPullsCreate ) . not . toHaveBeenCalled ( ) ;
@@ -366,27 +438,49 @@ describe("updateFromSourceSpec", () => {
366438
367439 it ( "should include rebase abort error in PR comment when abort fails" , async ( ) => {
368440 setupMocks ( { hasChanges : true , existingPRNumber : 42 } ) ;
441+ // First push via exec throws
369442 state . execImpl = async (
370443 cmd : string ,
371444 args ?: string [ ] ,
372445 ) : Promise < number > => {
373446 if (
374447 cmd === "git" &&
375448 Array . isArray ( args ) &&
376- ( args . includes ( "push" ) ||
377- ( args . includes ( "pull" ) && args . includes ( "--rebase" ) ) )
449+ args . includes ( "push" )
378450 ) {
379- throw new Error ( "merge conflict" ) ;
451+ throw new Error ( "rejected (non-fast-forward)" ) ;
452+ }
453+ return 0 ;
454+ } ;
455+ // Rebase fails AND abort fails via getExecOutput
456+ state . getExecOutputImpl = async (
457+ cmd : string ,
458+ args ?: string [ ] ,
459+ ) => {
460+ if (
461+ cmd === "git" &&
462+ Array . isArray ( args ) &&
463+ args . includes ( "--porcelain" )
464+ ) {
465+ return { stdout : "M openapi/openapi.json\n" , stderr : "" , exitCode : 0 } ;
466+ }
467+ if (
468+ cmd === "git" &&
469+ Array . isArray ( args ) &&
470+ args . includes ( "pull" ) &&
471+ args . includes ( "--rebase" )
472+ ) {
473+ return { stdout : "" , stderr : "merge conflict" , exitCode : 1 } ;
380474 }
381475 if (
382476 cmd === "git" &&
383477 Array . isArray ( args ) &&
384478 args . includes ( "rebase" ) &&
385479 args . includes ( "--abort" )
386480 ) {
387- throw new Error ( " no rebase in progress") ;
481+ return { stdout : "" , stderr : " no rebase in progress", exitCode : 1 } ;
388482 }
389- return 0 ;
483+ return { stdout : "" , stderr : "" , exitCode : 0 } ;
390484 } ;
391485 await importAndRun ( ) ;
392486
0 commit comments