Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m0s
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.
222 lines
11 KiB
PHP
222 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Filament\Widgets\ManagedEnvironment\ManagedEnvironmentReviewPackCard;
|
|
use App\Jobs\GenerateReviewPackJob;
|
|
use App\Models\Finding;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\User;
|
|
use App\Services\Findings\FindingExceptionService;
|
|
use App\Services\Findings\FindingRiskGovernanceResolver;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
function spec308SeedExpiredDecisionFinding(ManagedEnvironment $tenant, User $requester, string $title): Finding
|
|
{
|
|
$approver = User::factory()->create(['name' => 'Spec 308 Approver']);
|
|
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
|
|
|
|
/** @var FindingExceptionService $exceptionService */
|
|
$exceptionService = app(FindingExceptionService::class);
|
|
|
|
$finding = Finding::factory()->for($tenant)->riskAccepted()->create([
|
|
'fingerprint' => 'spec-308-raw-fingerprint-'.$title,
|
|
'evidence_jsonb' => [
|
|
'display_name' => $title,
|
|
'internal_url' => 'https://tenantpilot.test/admin/operations/raw-run',
|
|
],
|
|
]);
|
|
|
|
$requested = $exceptionService->request($finding, $tenant, $requester, [
|
|
'owner_user_id' => (int) $requester->getKey(),
|
|
'request_reason' => 'Temporary exception for staged remediation.',
|
|
'review_due_at' => now()->addDays(5)->toDateTimeString(),
|
|
'expires_at' => now()->addDays(14)->toDateTimeString(),
|
|
]);
|
|
|
|
$exceptionService->approve($requested, $approver, [
|
|
'effective_from' => now()->subDays(10)->toDateTimeString(),
|
|
'expires_at' => now()->subDay()->toDateTimeString(),
|
|
'approval_reason' => 'Approved with customer controls.',
|
|
]);
|
|
|
|
app(FindingRiskGovernanceResolver::class)->syncExceptionState($finding->findingException()->firstOrFail());
|
|
|
|
return $finding->refresh();
|
|
}
|
|
|
|
it('renders an executive-ready environment review and exports a pack with matching section order and summary truth', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = composeEnvironmentReviewForTest($tenant, $user);
|
|
|
|
$this->actingAs($user)
|
|
->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant))
|
|
->assertOk()
|
|
->assertSee('Executive posture')
|
|
->assertSee('Executive summary')
|
|
->assertSee('Open risk highlights')
|
|
->assertSee('Permission posture')
|
|
->assertSee('Publication readiness');
|
|
|
|
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
]);
|
|
|
|
$job = new GenerateReviewPackJob(
|
|
reviewPackId: (int) $pack->getKey(),
|
|
operationRunId: (int) $pack->operation_run_id,
|
|
);
|
|
app()->call([$job, 'handle']);
|
|
|
|
$pack->refresh();
|
|
$review->refresh()->load(['sections', 'evidenceSnapshot']);
|
|
|
|
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'environment-review-pack-');
|
|
file_put_contents($tempFile, $zipContent);
|
|
|
|
$zip = new ZipArchive;
|
|
$zip->open($tempFile);
|
|
|
|
$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);
|
|
$executiveEntrypoint = (string) $zip->getFromName(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
|
|
$filenames = collect(range(0, $zip->numFiles - 1))
|
|
->map(fn (int $index): string => (string) $zip->getNameIndex($index))
|
|
->values()
|
|
->all();
|
|
|
|
expect(array_column($sections, 'section_key'))
|
|
->toBe($review->sections->pluck('section_key')->values()->all())
|
|
->and($summary['highlights'] ?? null)->toBe($review->summary['highlights'] ?? [])
|
|
->and($summary['recommended_next_actions'] ?? null)->toBe($review->summary['recommended_next_actions'] ?? [])
|
|
->and($summary['delivery_bundle']['contract'] ?? null)->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
|
|
->and($summary['delivery_bundle']['executive_entrypoint_file'] ?? null)->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
|
|
->and($metadata['delivery_bundle']['contract'] ?? null)->toBe(ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT)
|
|
->and($metadata['delivery_bundle']['review_pack_id'] ?? null)->toBe((int) $pack->getKey())
|
|
->and($metadata['delivery_bundle']['released_review']['id'] ?? null)->toBe((int) $review->getKey())
|
|
->and($metadata['delivery_bundle']['interpretation_version'] ?? null)->toBe($review->controlInterpretationVersion())
|
|
->and($metadata['delivery_bundle']['entrypoint']['file'] ?? null)->toBe(ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
|
|
->and(array_slice(collect($metadata['delivery_bundle']['appendix'] ?? [])->pluck('file')->all(), 0, 3))->toBe(['metadata.json', 'summary.json', 'sections.json'])
|
|
->and(collect($metadata['delivery_bundle']['appendix'] ?? [])->pluck('file')->all())->toContain('sections/10-executive_summary.json')
|
|
->and($filenames)->toContain('metadata.json', 'summary.json', 'sections.json', ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME)
|
|
->and(collect($filenames)->filter(fn (string $filename): bool => str_starts_with($filename, 'executive-'))->values()->all())->toBe([ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME])
|
|
->and($executiveEntrypoint)->toContain('# Executive summary')
|
|
->and($executiveEntrypoint)->toContain('## Executive story')
|
|
->and($executiveEntrypoint)->toContain('## Structured auditor appendix')
|
|
->and($executiveEntrypoint)->toContain('metadata.json, summary.json, and sections.json')
|
|
->and($executiveEntrypoint)->not->toContain((string) $review->fingerprint)
|
|
->and($executiveEntrypoint)->not->toContain((string) $review->evidenceSnapshot?->fingerprint)
|
|
->and($executiveEntrypoint)->not->toContain('Reason owner')
|
|
->and($executiveEntrypoint)->not->toContain('Platform reason family');
|
|
|
|
$zip->close();
|
|
unlink($tempFile);
|
|
|
|
setAdminEnvironmentContext($tenant);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ManagedEnvironmentReviewPackCard::class, ['record' => $tenant])
|
|
->assertSee('View review');
|
|
});
|
|
|
|
it('builds an explicit customer-safe decision summary for released review consumption', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(
|
|
tenant: ManagedEnvironment::factory()->create(['name' => 'Visible Customer Environment']),
|
|
role: 'owner',
|
|
);
|
|
spec308SeedExpiredDecisionFinding($tenant, $user, 'Privileged role exception');
|
|
|
|
$hiddenTenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Hidden Customer Environment',
|
|
]);
|
|
[$hiddenUser, $hiddenTenant] = createUserWithTenant(tenant: $hiddenTenant, role: 'owner');
|
|
spec308SeedExpiredDecisionFinding($hiddenTenant, $hiddenUser, 'Hidden tenant exception');
|
|
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'published_at' => now(),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
|
|
$decisionSummary = data_get($review->fresh(), 'summary.governance_package.decision_summary');
|
|
|
|
expect($decisionSummary)->toBeArray()
|
|
->and($decisionSummary['status'] ?? null)->toBe('requires_awareness')
|
|
->and($decisionSummary['total_count'] ?? null)->toBe(1)
|
|
->and($decisionSummary['summary'] ?? null)->toContain('1 governance decision requires customer awareness')
|
|
->and($decisionSummary['next_action'] ?? null)->toBe('Review the accepted-risk decision basis before customer delivery.')
|
|
->and(data_get($decisionSummary, 'entries.0.title'))->toBe('Privileged role exception')
|
|
->and(data_get($decisionSummary, 'entries.0.awareness_reason'))->toContain('expired')
|
|
->and(data_get($decisionSummary, 'entries.0.next_action'))->toBe('Confirm whether this accepted risk should be renewed, remediated, or removed before relying on the review.')
|
|
->and(json_encode($decisionSummary, JSON_THROW_ON_ERROR))->not->toContain(
|
|
'Hidden tenant exception',
|
|
'spec-308-raw-fingerprint',
|
|
'raw-run',
|
|
'OperationRun',
|
|
'Reason owner',
|
|
'Platform reason family',
|
|
);
|
|
|
|
$this->actingAs($user)
|
|
->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?customer_workspace=1&source_surface=customer_review_workspace')
|
|
->assertOk()
|
|
->assertSee('Governance decisions requiring awareness')
|
|
->assertSee('Privileged role exception')
|
|
->assertSee('Review the accepted-risk decision basis before customer delivery.')
|
|
->assertDontSee('Hidden tenant exception')
|
|
->assertDontSee('raw-run')
|
|
->assertDontSeeText('Approve exception')
|
|
->assertDontSeeText('Reject exception')
|
|
->assertDontSeeText('Renew exception')
|
|
->assertDontSeeText('Revoke exception');
|
|
});
|
|
|
|
it('distinguishes no decision awareness from incomplete decision evidence', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$reviewWithNoDecisions = composeEnvironmentReviewForTest(
|
|
tenant: $tenant,
|
|
user: $user,
|
|
snapshot: seedEnvironmentReviewEvidence($tenant, findingCount: 2, driftCount: 0),
|
|
);
|
|
|
|
$noDecisionSummary = data_get($reviewWithNoDecisions, 'summary.governance_package.decision_summary');
|
|
|
|
expect($noDecisionSummary)->toBeArray()
|
|
->and($noDecisionSummary['status'] ?? null)->toBe('none')
|
|
->and($noDecisionSummary['total_count'] ?? null)->toBe(0)
|
|
->and($noDecisionSummary['summary'] ?? null)->toBe('No governance decisions require customer awareness in this released review.');
|
|
|
|
$partialTenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Partial Evidence Environment',
|
|
]);
|
|
createUserWithTenant(tenant: $partialTenant, user: $user, role: 'owner');
|
|
$partialReview = composeEnvironmentReviewForTest(
|
|
tenant: $partialTenant,
|
|
user: $user,
|
|
snapshot: seedPartialEnvironmentReviewEvidence($partialTenant, findingCount: 0, driftCount: 0),
|
|
);
|
|
|
|
$unavailableSummary = data_get($partialReview, 'summary.governance_package.decision_summary');
|
|
|
|
expect($unavailableSummary)->toBeArray()
|
|
->and($unavailableSummary['status'] ?? null)->toBe('unavailable')
|
|
->and($unavailableSummary['total_count'] ?? null)->toBe(0)
|
|
->and($unavailableSummary['summary'] ?? null)->toContain('Decision evidence is incomplete');
|
|
});
|