@@ -318,95 +318,42 @@ def in_progress_warning_detail
318318 expect { job . send ( :raise_if_sub_jobs_failed ) } . not_to raise_error
319319 end
320320
321- context 'when a settled sub-job has failed' do
322- before do
323- make_sub_job ( state : PollableJobModel ::FAILED_STATE ,
324- resource_type : 'service_credential_binding' , resource_guid : 'binding-1' ,
325- cf_api_error : YAML . dump ( { 'errors' => [ { 'title' => 'CF-UnableToPerform' , 'code' => 10_009 ,
326- 'detail' => 'unbind could not be completed: broker exploded' } ] } ) )
327- make_sub_job ( state : PollableJobModel ::COMPLETE_STATE )
328- end
329-
330- it 'raises a CompoundError of UnprocessableEntity carrying the failed sub-job detail' do
331- job . send ( :fetch_root_context )
332- expect { job . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
333- expect ( err . underlying_errors . map ( &:name ) ) . to eq ( %w[ UnprocessableEntity ] )
334- expect ( err . underlying_errors . first . message ) . to include ( 'unbind could not be completed: broker exploded' )
335- end
336- end
337-
338- context 'when the failed sub-job has no stored error detail' do
339- before do
340- make_sub_job ( state : PollableJobModel ::FAILED_STATE ,
341- resource_type : 'service_credential_binding' , resource_guid : 'binding-2' )
342- end
343-
344- it 'falls back to a resource reference for that entry' do
345- job . send ( :fetch_root_context )
346- expect { job . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
347- expect ( err . underlying_errors . map ( &:message ) ) . to include ( a_string_including ( 'service_credential_binding binding-2' ) )
348- end
349- end
350- end
321+ # The shape of the raised error (merge, dedup, detail parsing) is covered in sub_resource_failures_spec.
322+ it 'raises a CompoundError when a sub-job has settled failed' do
323+ make_sub_job ( state : PollableJobModel ::FAILED_STATE , resource_type : 'service_credential_binding' , resource_guid : 'binding-1' )
324+ job . send ( :fetch_root_context )
325+ expect { job . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError )
351326 end
352327 end
353328
354329 describe 'sub_resource_errors (durable sync-failure hook)' do
355330 let! ( :root_pollable_job ) { make_root }
356331
332+ let ( :job_with_sync_failure ) do
333+ klass = Class . new ( test_job_class ) do
334+ def sub_resource_errors
335+ [ [ 'sync-binding' , CloudController ::Errors ::ApiError . new_from_details ( 'UnprocessableEntity' , 'sync unbind failed' ) ] ]
336+ end
337+ end
338+ klass . new ( 'resource-guid-1' )
339+ end
340+
357341 it 'defaults to none so jobs without sub-resources are unaffected' do
358342 make_sub_job ( state : PollableJobModel ::COMPLETE_STATE )
359343 job . send ( :fetch_root_context )
360344 expect { job . send ( :raise_if_sub_jobs_failed ) } . not_to raise_error
361345 end
362346
363- context 'when a subclass reports a failed sub-resource with no matching sub-job' do
364- let ( :job_with_sync_failure ) do
365- klass = Class . new ( test_job_class ) do
366- def sub_resource_errors
367- [ [ 'sync-binding' , CloudController ::Errors ::ApiError . new_from_details ( 'UnprocessableEntity' , 'sync unbind failed' ) ] ]
368- end
369- end
370- klass . new ( 'resource-guid-1' )
371- end
372-
373- it 'does NOT halt when there is no failed sub-job, so the action can re-run and retry it' do
374- job_with_sync_failure . send ( :fetch_root_context )
375- expect { job_with_sync_failure . send ( :raise_if_sub_jobs_failed ) } . not_to raise_error
376- end
377-
378- it 'is reported once a sub-job has terminally failed (action is then skipped), merged with that sub-job' do
379- make_sub_job ( state : PollableJobModel ::FAILED_STATE ,
380- resource_type : 'service_credential_binding' , resource_guid : 'async-binding' ,
381- cf_api_error : YAML . dump ( { 'errors' => [ { 'detail' => 'async unbind failed' } ] } ) )
382- job_with_sync_failure . send ( :fetch_root_context )
383-
384- expect { job_with_sync_failure . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
385- expect ( err . underlying_errors . map ( &:message ) ) . to include ( a_string_including ( 'sync unbind failed' ) , a_string_including ( 'async unbind failed' ) )
386- end
387- end
347+ # Self-heal contract: a sync failure alone must not halt, so the action re-runs and retries it.
348+ it 'does NOT halt when a sub-resource failed but no sub-job has failed' do
349+ job_with_sync_failure . send ( :fetch_root_context )
350+ expect { job_with_sync_failure . send ( :raise_if_sub_jobs_failed ) } . not_to raise_error
388351 end
389352
390- context 'when a failed sub-resource shares its guid with a failed sub-job' do
391- let ( :job_with_dup ) do
392- klass = Class . new ( test_job_class ) do
393- def sub_resource_errors
394- [ [ 'shared-guid' , CloudController ::Errors ::ApiError . new_from_details ( 'UnprocessableEntity' , 'unbind failed once' ) ] ]
395- end
396- end
397- klass . new ( 'resource-guid-1' )
398- end
399-
400- it 'reports the resource only once' do
401- make_sub_job ( state : PollableJobModel ::FAILED_STATE ,
402- resource_type : 'service_credential_binding' , resource_guid : 'shared-guid' ,
403- cf_api_error : YAML . dump ( { 'errors' => [ { 'detail' => 'unbind failed once' } ] } ) )
404- job_with_dup . send ( :fetch_root_context )
405-
406- expect { job_with_dup . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
407- expect ( err . underlying_errors . size ) . to eq ( 1 )
408- end
409- end
353+ it 'halts once a sub-job has terminally failed, feeding the hook into the raised error' do
354+ make_sub_job ( state : PollableJobModel ::FAILED_STATE , resource_type : 'service_credential_binding' , resource_guid : 'async-binding' )
355+ job_with_sync_failure . send ( :fetch_root_context )
356+ expect { job_with_sync_failure . send ( :raise_if_sub_jobs_failed ) } . to raise_error ( CloudController ::Errors ::CompoundError )
410357 end
411358 end
412359
@@ -503,32 +450,13 @@ def sub_resource_errors
503450 expect ( logger ) . not_to have_received ( :warn ) . with ( a_string_including ( 'sub-resource deletion failed' ) )
504451 end
505452
506- it 'translates SubResourceError with real failures to a CompoundError of UnprocessableEntity' do
453+ # Routing only; the CompoundError's contents are covered in sub_resource_failures_spec.
454+ it 'translates a SubResourceError with real failures to a CompoundError' do
507455 expect do
508456 job . send ( :perform_with_root_job_handling ) do
509457 raise SubResourceError . new ( [ StandardError . new ( 'one broke' ) , StandardError . new ( 'two broke' ) ] )
510458 end
511- end . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
512- expect ( err . underlying_errors ) . to all ( be_a ( CloudController ::Errors ::ApiError ) )
513- expect ( err . underlying_errors . map ( &:name ) ) . to eq ( %w[ UnprocessableEntity UnprocessableEntity ] )
514- expect ( err . underlying_errors . map ( &:message ) ) . to include ( match ( /one broke/ ) , match ( /two broke/ ) )
515- end
516- end
517-
518- it 'merges current-tick sync failures with settled failed sub-jobs into one CompoundError' do
519- make_sub_job ( state : PollableJobModel ::FAILED_STATE ,
520- resource_type : 'service_credential_binding' , resource_guid : 'async-binding' ,
521- cf_api_error : YAML . dump ( { 'errors' => [ { 'title' => 'CF-UnableToPerform' , 'code' => 10_009 ,
522- 'detail' => 'async unbind failed' } ] } ) )
523-
524- expect do
525- job . send ( :perform_with_root_job_handling ) do
526- raise SubResourceError . new ( [ StandardError . new ( 'sync unbind failed' ) ] )
527- end
528- end . to raise_error ( CloudController ::Errors ::CompoundError ) do |err |
529- expect ( err . underlying_errors . map ( &:name ) ) . to all ( eq ( 'UnprocessableEntity' ) )
530- expect ( err . underlying_errors . map ( &:message ) ) . to include ( match ( /sync unbind failed/ ) , match ( /async unbind failed/ ) )
531- end
459+ end . to raise_error ( CloudController ::Errors ::CompoundError )
532460 end
533461
534462 it 'passes ApiErrors through unchanged' do
0 commit comments