TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackReadinessSemanticsTest.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

112 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\GenerateReviewPackJob;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\ReviewPackService;
use App\Support\ReviewPackStatus;
use Illuminate\Support\Facades\Storage;
beforeEach(function (): void {
Storage::fake('exports');
});
it('keeps section appendix files even when readiness stays limitation-aware', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => false,
'include_operations' => true,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
), 'handle']);
$pack->refresh();
[$zip, $tempFile, $filenames] = spec347ReadinessZip($pack);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$sections = collect(json_decode((string) $zip->getFromName('sections.json'), true, 512, JSON_THROW_ON_ERROR));
$executive = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
$limitedSection = $sections->first(fn (array $section): bool => (string) $section['completeness_state'] !== 'complete');
expect($pack->status)->toBe(ReviewPackStatus::Ready->value)
->and(data_get($summary, 'has_ready_export'))->toBeTrue()
->and(data_get($summary, 'output_readiness.readiness_state'))->toBe('published_with_limitations')
->and(data_get($summary, 'output_readiness.evidence_completeness_state'))->toBe((string) $snapshot->completeness_state)
->and((int) data_get($summary, 'output_readiness.section_summary.required_limited'))->toBeGreaterThan(0)
->and($limitedSection)->not->toBeNull();
$limitedSectionFilename = sprintf(
'sections/%02d-%s.json',
(int) $limitedSection['sort_order'],
(string) $limitedSection['section_key'],
);
expect($filenames)->toContain($limitedSectionFilename)
->and($executive)->toContain('## Limitations')
->and($executive)->toContain('incomplete evidence basis')
->and($executive)->toContain('structured appendices but are marked missing');
$zip->close();
unlink($tempFile);
});
it('distinguishes ready export from customer-safe readiness when pii is included', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $user);
$review = markEnvironmentReviewCustomerSafeReady($review);
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
'include_pii' => true,
'include_operations' => true,
]);
app()->call([new GenerateReviewPackJob(
reviewPackId: (int) $pack->getKey(),
operationRunId: (int) $pack->operation_run_id,
), 'handle']);
$pack->refresh();
[$zip, $tempFile] = spec347ReadinessZip($pack);
$summary = json_decode((string) $zip->getFromName('summary.json'), true, 512, JSON_THROW_ON_ERROR);
$executive = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
expect(data_get($summary, 'has_ready_export'))->toBeTrue()
->and(data_get($summary, 'output_readiness.readiness_state'))->toBe('internal_review_package_available')
->and(data_get($summary, 'output_readiness.customer_safe_state'))->toBe('internal_only')
->and(data_get($summary, 'output_readiness.contains_pii'))->toBeTrue()
->and($executive)->toContain('PII is included in this package');
$zip->close();
unlink($tempFile);
});
/**
* @return array{0: ZipArchive, 1: string, 2: list<string>}
*/
function spec347ReadinessZip(ReviewPack $pack): array
{
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
$tempFile = tempnam(sys_get_temp_dir(), 'spec347-readiness-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];
}