TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec385ReviewPackBaselineReadinessTest.php
Ahmed Darrazi d71493f82a
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m44s
feat(evidence): implement baseline review readiness integration
Added BaselineReadinessGate, resolution propagation, and disclosure semantics logic per Spec 385. Integrated baseline unreadiness into Customer Review Workspace and Review Packs.
2026-06-18 00:52:53 +02:00

281 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\GenerateReviewPackJob;
use App\Services\ReviewPackService;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\ReviewPacks\ReportDisclosurePolicy;
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
use Illuminate\Support\Facades\Storage;
beforeEach(function (): void {
Storage::fake('exports');
});
it('maps baseline readiness blockers into publication-blocked review pack guidance', function (): void {
$readiness = spec385ReviewPackReadiness([
'state' => 'partial',
'readiness_state' => 'baseline_identity_unresolved',
'publication_blockers' => [
'Baseline subject identity must be resolved before customer-ready publication.',
],
'customer_safe_summary' => [
'readiness_state' => 'baseline_identity_unresolved',
'verified_subject_count' => 0,
'drift_subject_count' => 0,
'blocker_count' => 1,
'limitation_count' => 0,
],
]);
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
'review' => '/reviews/1',
'evidence' => '/evidence/1',
]);
expect($guidance['state'])->toBe(ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED)
->and($guidance['limitations'][0]['key'])->toBe('baseline_publication_blockers_present')
->and($guidance['limitations'][0]['label'])->toBe('Baseline readiness blocked')
->and($guidance['primary_action']['label'])->toBe('Open baseline resolution')
->and($guidance['technical_details']['Baseline readiness'])->toContain('Baseline subject identity unresolved')
->and(json_encode($guidance, JSON_THROW_ON_ERROR))->not->toContain('baseline_identity_unresolved')
->and($guidance)->not->toHaveKey('provider_resource_bindings');
});
it('maps accepted baseline limitations into published-with-limitations guidance', function (): void {
$readiness = spec385ReviewPackReadiness([
'state' => 'partial',
'readiness_state' => 'baseline_compare_limited',
'publication_blockers' => [],
'limitation_codes' => ['baseline_accepted_limitations'],
'customer_safe_summary' => [
'readiness_state' => 'baseline_compare_limited',
'verified_subject_count' => 0,
'drift_subject_count' => 0,
'blocker_count' => 0,
'limitation_count' => 1,
],
]);
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
'review' => '/reviews/1',
]);
expect($guidance['state'])->toBe(ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS)
->and(collect($guidance['limitations'])->pluck('key')->all())->toContain('baseline_accepted_limitations')
->and($guidance['primary_reason'])->toBe('Baseline limitations qualify this output.');
});
it('adds baseline readiness to customer-facing report disclosure proof', function (): void {
$readiness = spec385ReviewPackReadiness([
'state' => 'partial',
'readiness_state' => 'baseline_identity_unresolved',
'publication_blockers' => [
'Baseline subject identity must be resolved before customer-ready publication.',
],
'customer_safe_summary' => [
'readiness_state' => 'baseline_identity_unresolved',
'verified_subject_count' => 0,
'drift_subject_count' => 0,
'blocker_count' => 1,
'limitation_count' => 0,
],
]);
$policy = ReportDisclosurePolicy::evaluate([
'is_customer_facing' => true,
'audience_label' => 'Customer executive',
'show_section_appendix' => false,
'show_technical_details' => false,
], $readiness);
expect($policy['proof_states']['baseline_readiness'])->toBe(ReportDisclosurePolicy::PROOF_MISSING)
->and(collect($policy['blocking_reasons'])->pluck('key')->all())->toContain('baseline_readiness_blocked')
->and(collect($policy['mandatory_disclosures'])->pluck('key')->all())->toContain('baseline_readiness');
});
it('maps stale failed and unproven baseline proof to explicit limitation codes', function (
string $readinessState,
string $expectedCode,
string $expectedAction,
): void {
$readiness = spec385ReviewPackReadiness([
'state' => in_array($readinessState, ['baseline_compare_failed', 'baseline_compare_unproven'], true) ? 'missing' : 'stale',
'readiness_state' => $readinessState,
'publication_blockers' => [],
'customer_safe_summary' => [
'readiness_state' => $readinessState,
'verified_subject_count' => 0,
'drift_subject_count' => 0,
'blocker_count' => 0,
'limitation_count' => 0,
],
]);
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
'review' => '/reviews/1',
'evidence' => '/evidence/1',
'operation' => '/operations/1',
]);
expect(collect($guidance['limitations'])->pluck('key')->all())->toContain($expectedCode)
->and($guidance['primary_action']['key'])->toBe($expectedAction);
})->with([
'unproven compare' => ['baseline_compare_unproven', 'baseline_compare_unproven', 'open_evidence_basis'],
'stale compare' => ['baseline_compare_stale', 'baseline_compare_stale', 'open_evidence_basis'],
'failed compare' => ['baseline_compare_failed', 'baseline_compare_failed', 'open_operation_proof'],
]);
it('redacts baseline internal diagnostics from customer-safe review pack output but keeps them for internal output', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
$baselineReadiness = [
'version' => 'baseline_readiness.spec385.v1',
'state' => 'partial',
'readiness_state' => 'baseline_identity_unresolved',
'publication_blockers' => [
'Baseline subject identity must be resolved before customer-ready publication.',
],
'limitations' => [
[
'code' => 'baseline_accepted_limitations',
'summary' => 'Accepted baseline limitations qualify the customer-ready claim.',
],
],
'limitation_codes' => ['baseline_accepted_limitations'],
'customer_safe_summary' => [
'readiness_state' => 'baseline_identity_unresolved',
'verified_subject_count' => 1,
'drift_subject_count' => 0,
'blocker_count' => 1,
'limitation_count' => 1,
],
'internal_diagnostics' => [
'latest_compare_run_id' => 12345,
'binding_decision_counts' => ['exact_provider_identity' => 1],
'provider_resource_id' => 'provider-policy-123',
'canonical_subject_key' => 'baseline:policy:provider-policy-123',
],
];
$review->forceFill([
'summary' => array_replace(is_array($review->summary) ? $review->summary : [], [
'baseline_readiness' => $baselineReadiness,
'baseline_publication_blockers' => [],
'baseline_limitations' => [],
'publish_blockers' => [],
]),
])->save();
$customerPack = app(ReviewPackService::class)->generateFromReview($review->fresh(), $user, [
'include_pii' => false,
'include_operations' => false,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $customerPack->getKey(),
operationRunId: (int) $customerPack->operation_run_id,
), 'handle']);
$internalPack = app(ReviewPackService::class)->generateFromReview($review->fresh(), $user, [
'include_pii' => true,
'include_operations' => false,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $internalPack->getKey(),
operationRunId: (int) $internalPack->operation_run_id,
), 'handle']);
$customerPack->refresh();
$internalPack->refresh();
$customerSummaryJson = json_encode(spec385ZipJson($customerPack, 'summary.json'), JSON_THROW_ON_ERROR);
$customerMetadataJson = json_encode(spec385ZipJson($customerPack, 'metadata.json'), JSON_THROW_ON_ERROR);
$customerExecutiveMarkdown = spec385ZipText($customerPack, ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
$internalSummaryJson = json_encode(spec385ZipJson($internalPack, 'summary.json'), JSON_THROW_ON_ERROR);
expect($customerSummaryJson)->toContain('baseline_readiness')
->and($customerPack->file_path)->not->toBe($internalPack->file_path)
->and($customerSummaryJson)->toContain('Baseline subject identity unresolved', 'Published with limitations')
->and($customerSummaryJson)->not->toContain(
'internal_diagnostics',
'latest_compare_run_id',
'binding_decision_counts',
'provider_resource_id',
'canonical_subject_key',
'baseline_readiness.spec385.v1',
'baseline_identity_unresolved',
'baseline_publication_blockers_present',
'baseline_accepted_limitations',
'environment_review_id',
'snapshot_id',
'review_pack_id',
'"id":',
)
->and($customerMetadataJson)->not->toContain(
'internal_diagnostics',
'latest_compare_run_id',
'binding_decision_counts',
'provider_resource_id',
'canonical_subject_key',
'baseline_readiness.spec385.v1',
'baseline_identity_unresolved',
'baseline_publication_blockers_present',
'baseline_accepted_limitations',
'environment_review_id',
'snapshot_id',
'review_pack_id',
'"id":',
)
->and($customerExecutiveMarkdown)->toContain('current released review', 'Accepted baseline limitations qualify the customer-ready claim.')
->and($customerExecutiveMarkdown)->not->toContain('#'.$review->getKey(), '#'.$review->evidence_snapshot_id, 'baseline_identity_unresolved', 'baseline_accepted_limitations')
->and($internalSummaryJson)->toContain('internal_diagnostics', 'latest_compare_run_id', 'binding_decision_counts', 'baseline_identity_unresolved');
});
/**
* @param array<string, mixed> $baselineReadiness
* @return array<string, mixed>
*/
function spec385ReviewPackReadiness(array $baselineReadiness): array
{
return ReviewPackOutputReadiness::derive(
reviewStatus: 'published',
reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->value,
evidenceCompletenessState: EnvironmentReviewCompletenessState::Complete->value,
sectionStateCounts: [EnvironmentReviewCompletenessState::Complete->value => 6],
requiredSectionCount: 6,
requiredSectionStateCounts: [EnvironmentReviewCompletenessState::Complete->value => 6],
publishBlockers: [],
hasReadyExport: true,
includePii: false,
protectedValuesHidden: true,
disclosurePresent: true,
baselineReadiness: $baselineReadiness,
);
}
/**
* @return array<string, mixed>
*/
function spec385ZipJson(\App\Models\ReviewPack $pack, string $filename): array
{
$payload = json_decode(spec385ZipText($pack, $filename), true, 512, JSON_THROW_ON_ERROR);
return is_array($payload) ? $payload : [];
}
function spec385ZipText(\App\Models\ReviewPack $pack, string $filename): string
{
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'spec385-review-pack-');
file_put_contents($tempFile, $zipContent);
$zip = new ZipArchive;
$zip->open($tempFile);
$payload = (string) $zip->getFromName($filename);
$zip->close();
unlink($tempFile);
return $payload;
}