$budgetOutcome * @return array */ function spec439SuccessfulExecutionWithBudget(array $budgetOutcome): array { return TestLaneReport::deriveFinalResult( execution: TestLaneReport::executionResult(0), evidence: [ 'path' => '/tmp/spec439-budget.junit.xml', 'exists' => true, 'parseValid' => true, 'testCount' => 1, 'failureCount' => 0, 'errorCount' => 0, 'skippedCount' => 0, 'status' => 'passed', ], reportGeneration: ['status' => 'succeeded'], artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []], budgetOutcome: $budgetOutcome, ); } it('classifies wrapper or manifest drift before lower-level lane failures', function (): void { $primaryFailureClassId = TestLaneReport::classifyPrimaryFailure( exitCode: 1, artifactPublicationStatus: ['complete' => true], budgetOutcome: ['budgetStatus' => 'over-budget'], entryPointResolved: false, workflowLaneMatched: false, ); $finalResult = TestLaneReport::deriveFinalResult( execution: TestLaneReport::executionResult(0), evidence: [ 'status' => 'passed', 'testCount' => 1, 'failureCount' => 0, 'errorCount' => 0, 'skippedCount' => 0, ], reportGeneration: ['status' => 'succeeded'], artifactPublicationStatus: ['complete' => true], entryPointResolved: false, workflowLaneMatched: false, ); expect($primaryFailureClassId)->toBe('wrapper-failure') ->and($finalResult['exitCode'])->toBe(1) ->and($finalResult['failureContextIds'])->toContain('wrapper-failure'); }); it('[EF-004] keeps confidence budget overruns visible without converting them into blocking failures', function (): void { $budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 481.0); $finalResult = spec439SuccessfulExecutionWithBudget($budgetOutcome); $summary = TestLaneReport::buildCiSummary( report: [ 'laneId' => 'confidence', 'budgetStatus' => 'within-budget', 'ciContext' => ['workflowId' => 'main-confidence'], 'execution' => TestLaneReport::executionResult(0), 'evidence' => ['status' => 'passed'], 'finalResult' => $finalResult, ], exitCode: 0, budgetOutcome: $budgetOutcome, artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []], ); expect($budgetOutcome['budgetStatus'])->toBe('over-budget') ->and($finalResult['exitCode'])->toBe(0) ->and($finalResult['status'])->toBe('passed') ->and($finalResult['blockingStatus'])->toBe('non-blocking-warning') ->and($finalResult['advisoryContextIds'])->toContain('budget-advisory-warning') ->and($summary['primaryFailureClassId'])->toBe('budget-breach') ->and($summary['blockingStatus'])->toBe('non-blocking-warning') ->and($summary['finalExitCode'])->toBe(0); }); it('[EF-003] treats mature fast-feedback budget overruns as blocking with a non-zero final exit', function (): void { $budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 216.0); $finalResult = spec439SuccessfulExecutionWithBudget($budgetOutcome); $summary = TestLaneReport::buildCiSummary( report: [ 'laneId' => 'fast-feedback', 'budgetStatus' => 'within-budget', 'ciContext' => ['workflowId' => 'pr-fast-feedback'], 'execution' => TestLaneReport::executionResult(0), 'evidence' => ['status' => 'passed'], 'finalResult' => $finalResult, ], exitCode: 0, budgetOutcome: $budgetOutcome, artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []], ); expect($budgetOutcome['budgetStatus'])->toBe('over-budget') ->and($finalResult['exitCode'])->toBe(1) ->and($finalResult['status'])->toBe('failed') ->and($finalResult['failureContextIds'])->toContain('blocking-budget-breach') ->and($summary['primaryFailureClassId'])->toBe('budget-breach') ->and($summary['blockingStatus'])->toBe('blocking') ->and($summary['finalExitCode'])->toBe(1); }); it('classifies incomplete artifact bundles independently from test and budget status', function (): void { $artifactPublicationStatus = [ 'complete' => false, 'publishedArtifacts' => [], 'missingRequiredArtifacts' => ['report.json'], ]; $finalResult = TestLaneReport::deriveFinalResult( execution: TestLaneReport::executionResult(0), evidence: [ 'status' => 'passed', 'testCount' => 1, 'failureCount' => 0, 'errorCount' => 0, 'skippedCount' => 0, ], reportGeneration: ['status' => 'succeeded'], artifactPublicationStatus: $artifactPublicationStatus, ); $summary = TestLaneReport::buildCiSummary( report: [ 'laneId' => 'fast-feedback', 'budgetStatus' => 'within-budget', 'ciContext' => ['workflowId' => 'pr-fast-feedback'], 'execution' => TestLaneReport::executionResult(0), 'evidence' => ['status' => 'passed'], 'finalResult' => $finalResult, ], exitCode: 0, budgetOutcome: null, artifactPublicationStatus: $artifactPublicationStatus, ); expect($summary['primaryFailureClassId'])->toBe('artifact-publication-failure') ->and($summary['blockingStatus'])->toBe('blocking') ->and($finalResult['status'])->toBe('failed') ->and($finalResult['exitCode'])->toBe(1) ->and($finalResult['failureContextIds'])->toContain('artifact-publication-failure') ->and($summary['finalExitCode'])->toBe(1); }); it('surfaces classification-only scope boundaries for heavy governance and browser reports', function (): void { $heavySummary = TestLaneReport::buildCiSummary( report: [ 'laneId' => 'heavy-governance', 'budgetStatus' => 'within-budget', 'ciContext' => ['workflowId' => 'heavy-governance-manual'], ], exitCode: 1, artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []], ); $browserSummary = TestLaneReport::buildCiSummary( report: [ 'laneId' => 'browser', 'budgetStatus' => 'within-budget', 'ciContext' => ['workflowId' => 'browser-manual'], ], exitCode: 1, artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []], ); expect($heavySummary['scopeBoundaryNote'])->toContain('full-suite repair ownership') ->and($browserSummary['scopeBoundaryNote'])->toContain('classification-only'); });