@@ -4,6 +4,7 @@ import type {
44 HelperApplyOptions ,
55 Pipeline ,
66 PipelineDiagnostic ,
7+ PipelineExtensionRollbackErrorMetadata ,
78 PipelineReporter ,
89 PipelineRunState ,
910} from '../types.js' ;
@@ -45,7 +46,28 @@ function createTestReporter(): TestReporter {
4546 return reporter ;
4647}
4748
48- function createTestPipeline ( ) : {
49+ function createTestPipeline ( options ?: {
50+ readonly onDiagnostic ?: ( {
51+ reporter,
52+ diagnostic,
53+ } : {
54+ readonly reporter : TestReporter ;
55+ readonly diagnostic : TestDiagnostic ;
56+ } ) => void ;
57+ readonly onExtensionRollbackError ?: ( {
58+ error,
59+ extensionKeys,
60+ hookSequence,
61+ errorMetadata,
62+ context,
63+ } : {
64+ readonly error : unknown ;
65+ readonly extensionKeys : readonly string [ ] ;
66+ readonly hookSequence : readonly string [ ] ;
67+ readonly errorMetadata : PipelineExtensionRollbackErrorMetadata ;
68+ readonly context : TestContext ;
69+ } ) => void ;
70+ } ) : {
4971 pipeline : TestPipeline ;
5072 reporter : TestReporter ;
5173} {
@@ -109,6 +131,8 @@ function createTestPipeline(): {
109131 createRunResult ( { artifact, diagnostics, steps } ) {
110132 return { artifact, diagnostics, steps } satisfies TestRunResult ;
111133 } ,
134+ onDiagnostic : options ?. onDiagnostic ,
135+ onExtensionRollbackError : options ?. onExtensionRollbackError ,
112136 } ) ;
113137
114138 pipeline . ir . use (
@@ -184,4 +208,127 @@ describe('createPipeline (extensions)', () => {
184208
185209 await expect ( pipeline . run ( { } ) ) . rejects . toThrow ( 'registration failed' ) ;
186210 } ) ;
211+
212+ it ( 'reports rollback metadata via the extension coordinator when builders fail' , async ( ) => {
213+ const rollback = jest . fn ( ( ) => {
214+ throw new Error ( 'rollback failure' ) ;
215+ } ) ;
216+ const onExtensionRollbackError = jest . fn ( ) ;
217+ const { pipeline, reporter } = createTestPipeline ( {
218+ onExtensionRollbackError,
219+ } ) ;
220+
221+ pipeline . extensions . use ( {
222+ key : 'test.rollback' ,
223+ register ( ) {
224+ return ( { artifact } : { artifact : string [ ] } ) => ( {
225+ artifact : [ ...artifact , 'extension' ] ,
226+ rollback,
227+ } ) ;
228+ } ,
229+ } ) ;
230+
231+ pipeline . builders . use (
232+ createHelper <
233+ TestContext ,
234+ void ,
235+ string [ ] ,
236+ TestReporter ,
237+ typeof pipeline . builderKind
238+ > ( {
239+ key : 'builder.failure' ,
240+ kind : pipeline . builderKind ,
241+ priority : 1 ,
242+ apply ( ) {
243+ throw new Error ( 'builder exploded' ) ;
244+ } ,
245+ } )
246+ ) ;
247+
248+ try {
249+ await pipeline . run ( { } ) ;
250+ throw new Error ( 'expected pipeline.run() to reject' ) ;
251+ } catch ( error ) {
252+ expect ( error ) . toBeInstanceOf ( Error ) ;
253+ expect ( ( error as Error ) . message ) . toBe ( 'builder exploded' ) ;
254+ }
255+
256+ expect ( rollback ) . toHaveBeenCalledTimes ( 1 ) ;
257+ expect ( onExtensionRollbackError ) . toHaveBeenCalledTimes ( 1 ) ;
258+
259+ const event = onExtensionRollbackError . mock . calls [ 0 ] [ 0 ] ;
260+ expect ( event . error ) . toBeInstanceOf ( Error ) ;
261+ expect ( ( event . error as Error ) . message ) . toBe ( 'rollback failure' ) ;
262+ expect ( event . extensionKeys ) . toEqual ( [ 'test.rollback' ] ) ;
263+ expect ( event . hookSequence ) . toEqual ( [ 'test.rollback' ] ) ;
264+ expect ( event . errorMetadata ) . toEqual (
265+ expect . objectContaining ( {
266+ message : 'rollback failure' ,
267+ name : 'Error' ,
268+ } )
269+ ) ;
270+ expect ( event . context . reporter ) . toBe ( reporter ) ;
271+ } ) ;
272+ } ) ;
273+
274+ describe ( 'createPipeline diagnostics' , ( ) => {
275+ it ( 'replays stored diagnostics once when a reporter is available' , async ( ) => {
276+ const onDiagnostic = jest . fn ( ) ;
277+ const { pipeline, reporter } = createTestPipeline ( { onDiagnostic } ) ;
278+
279+ pipeline . ir . use (
280+ createHelper <
281+ TestContext ,
282+ void ,
283+ string [ ] ,
284+ TestReporter ,
285+ typeof pipeline . fragmentKind
286+ > ( {
287+ key : 'fragment.conflict' ,
288+ kind : pipeline . fragmentKind ,
289+ mode : 'override' ,
290+ apply ( { output } ) {
291+ output . push ( 'first' ) ;
292+ } ,
293+ } )
294+ ) ;
295+
296+ expect ( ( ) =>
297+ pipeline . ir . use (
298+ createHelper <
299+ TestContext ,
300+ void ,
301+ string [ ] ,
302+ TestReporter ,
303+ typeof pipeline . fragmentKind
304+ > ( {
305+ key : 'fragment.conflict' ,
306+ kind : pipeline . fragmentKind ,
307+ mode : 'override' ,
308+ apply ( { output } ) {
309+ output . push ( 'second' ) ;
310+ } ,
311+ } )
312+ )
313+ ) . toThrow (
314+ 'Multiple overrides registered for helper "fragment.conflict".'
315+ ) ;
316+
317+ expect ( onDiagnostic ) . not . toHaveBeenCalled ( ) ;
318+
319+ await pipeline . run ( { } ) ;
320+
321+ expect ( onDiagnostic ) . toHaveBeenCalledTimes ( 1 ) ;
322+ expect ( onDiagnostic ) . toHaveBeenCalledWith ( {
323+ reporter,
324+ diagnostic : expect . objectContaining ( {
325+ type : 'conflict' ,
326+ key : 'fragment.conflict' ,
327+ } ) ,
328+ } ) ;
329+
330+ await pipeline . run ( { } ) ;
331+
332+ expect ( onDiagnostic ) . toHaveBeenCalledTimes ( 1 ) ;
333+ } ) ;
187334} ) ;
0 commit comments