TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackOutputContractTest.php
ahmido 12ea7f9924 feat: review pack output contract and readiness semantics (spec 347/348) (#419)
Implemented the output contract and readiness semantics for review packs. Also added spec 348.
Includes changes to ChooseEnvironment, CustomerReviewWorkspace, GenerateReviewPackJob and related blade views.
Added comprehensive tests.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #419
2026-06-02 23:17:08 +00:00

102 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\GenerateReviewPackJob;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\ReviewPackService;
use Illuminate\Support\Facades\Storage;
beforeEach(function (): void {
Storage::fake('exports');
});
it('captures the spec347 required review pack files and metadata contract', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => true,
'include_operations' => false,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
), 'handle']);
$pack->refresh();
[$zip, $tempFile, $filenames] = spec347OpenPackZip($pack);
$metadata = json_decode((string) $zip->getFromName('metadata.json'), true, 512, JSON_THROW_ON_ERROR);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$sections = json_decode((string) $zip->getFromName('sections.json'), true, 512, JSON_THROW_ON_ERROR);
expect($filenames)->toContain(
'metadata.json',
'summary.json',
'sections.json',
ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME,
)->and(data_get($metadata, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and(data_get($metadata, 'delivery_bundle.artifact_family'))->toBe('review_pack')
->and(data_get($metadata, 'delivery_bundle.review_pack_id'))->toBe((int) $pack->getKey())
->and(data_get($metadata, 'delivery_bundle.released_review.id'))->toBe((int) $review->getKey())
->and(data_get($metadata, 'delivery_bundle.released_review.status'))->toBe((string) $review->status)
->and(data_get($metadata, 'delivery_bundle.entrypoint.file'))->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
->and(data_get($metadata, 'delivery_bundle.entrypoint.role'))->toBe('executive_entrypoint')
->and(data_get($metadata, 'delivery_bundle.appendix.0.file'))->toBe('metadata.json')
->and(data_get($metadata, 'environment_review.id'))->toBe((int) $review->getKey())
->and(data_get($metadata, 'environment_review.status'))->toBe((string) $review->status)
->and(data_get($metadata, 'evidence_snapshot.id'))->toBe((int) $review->evidence_snapshot_id)
->and(data_get($metadata, 'evidence_snapshot.fingerprint'))->toBe((string) $review->evidenceSnapshot?->fingerprint)
->and(data_get($metadata, 'options.include_pii'))->toBeTrue()
->and(data_get($metadata, 'options.include_operations'))->toBeFalse()
->and(data_get($metadata, 'redaction_integrity.protected_values_hidden'))->toBeTrue()
->and(data_get($summary, 'has_ready_export'))->toBeTrue()
->and(data_get($summary, 'delivery_bundle.contract'))->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
->and(data_get($summary, 'evidence_basis.completeness_state'))->toBe((string) $review->evidenceSnapshot?->completeness_state)
->and(data_get($summary, 'output_readiness.has_ready_export'))->toBeTrue()
->and(data_get($summary, 'output_readiness.contains_pii'))->toBeTrue()
->and($sections)->toBeArray()->not->toBeEmpty();
$firstSection = $sections[0];
$firstSectionFilename = sprintf(
'sections/%02d-%s.json',
(int) $firstSection['sort_order'],
(string) $firstSection['section_key'],
);
$firstSectionFile = json_decode((string) $zip->getFromName($firstSectionFilename), true, 512, JSON_THROW_ON_ERROR);
expect($filenames)->toContain($firstSectionFilename)
->and($firstSectionFile['section_key'] ?? null)->toBe($firstSection['section_key'])
->and($firstSectionFile['title'] ?? null)->toBe($firstSection['title'])
->and($firstSectionFile['sort_order'] ?? null)->toBe($firstSection['sort_order'])
->and($firstSectionFile['required'] ?? null)->toBe($firstSection['required'])
->and($firstSectionFile['completeness_state'] ?? null)->toBe($firstSection['completeness_state']);
$zip->close();
unlink($tempFile);
});
/**
* @return array{0: ZipArchive, 1: string, 2: list<string>}
*/
function spec347OpenPackZip(ReviewPack $pack): array
{
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'spec347-review-pack-');
file_put_contents($tempFile, $zipContent);
$zip = new ZipArchive;
$zip->open($tempFile);
$filenames = collect(range(0, $zip->numFiles - 1))
->map(fn (int $index): string => (string) $zip->getNameIndex($index))
->values()
->all();
return [$zip, $tempFile, $filenames];
}