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
405 lines
19 KiB
PHP
405 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\User;
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
it('Spec342 smokes final customer review consumption states', function (): void {
|
|
[$user, $notReadyEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$notReadyEnvironment->forceFill(['name' => 'Spec342 Browser Evidence Incomplete'])->save();
|
|
|
|
$readyEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Ready');
|
|
$findingsEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Findings');
|
|
$acceptedRiskEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Accepted Risks');
|
|
|
|
spec342BrowserCreatePublishedReviewWithPack(
|
|
$notReadyEnvironment,
|
|
$user,
|
|
seedPartialEnvironmentReviewEvidence($notReadyEnvironment, findingCount: 0, driftCount: 0),
|
|
[
|
|
'debug_payload' => 'raw payload should stay hidden',
|
|
'provider_response' => 'provider response should stay hidden',
|
|
'stack_trace' => 'stack trace should stay hidden',
|
|
'source_fingerprint' => 'spec342-browser-hidden-fingerprint',
|
|
'governance_package' => [
|
|
'decision_summary' => [
|
|
'status' => 'incomplete',
|
|
'evidence_state' => EnvironmentReviewCompletenessState::Partial->value,
|
|
'decision_data_state' => 'incomplete',
|
|
'total_count' => 1,
|
|
'summary' => 'Decision evidence is incomplete for this released review.',
|
|
'next_action' => 'Review the evidence basis before relying on the decision summary.',
|
|
'entries' => [],
|
|
],
|
|
],
|
|
],
|
|
'review-packs/spec342-browser-evidence-incomplete.zip',
|
|
normalizeOutputReadiness: false,
|
|
);
|
|
|
|
spec342BrowserCreatePublishedReviewWithPack(
|
|
$readyEnvironment,
|
|
$user,
|
|
seedEnvironmentReviewEvidence($readyEnvironment, findingCount: 0, driftCount: 0),
|
|
[],
|
|
'review-packs/spec342-browser-ready.zip',
|
|
);
|
|
|
|
spec342BrowserCreatePublishedReviewWithPack(
|
|
$findingsEnvironment,
|
|
$user,
|
|
seedEnvironmentReviewEvidence($findingsEnvironment, findingCount: 0, driftCount: 0),
|
|
[],
|
|
'review-packs/spec342-browser-findings.zip',
|
|
);
|
|
Finding::factory()->create([
|
|
'managed_environment_id' => (int) $findingsEnvironment->getKey(),
|
|
'workspace_id' => (int) $findingsEnvironment->workspace_id,
|
|
'severity' => Finding::SEVERITY_CRITICAL,
|
|
'status' => Finding::STATUS_NEW,
|
|
]);
|
|
|
|
spec342BrowserCreatePublishedReviewWithPack(
|
|
$acceptedRiskEnvironment,
|
|
$user,
|
|
seedEnvironmentReviewEvidence($acceptedRiskEnvironment, findingCount: 0, driftCount: 0),
|
|
[
|
|
'governance_package' => [
|
|
'accepted_risks' => [
|
|
[
|
|
'title' => 'Accepted risk renewal',
|
|
'governance_state' => 'expiring_exception',
|
|
'customer_summary' => 'Accepted risk requires customer awareness.',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'review-packs/spec342-browser-accepted-risk.zip',
|
|
);
|
|
spec342BrowserCreateAcceptedRisk($acceptedRiskEnvironment, $user);
|
|
|
|
spec342AuthenticateBrowser($this, $user, $notReadyEnvironment);
|
|
|
|
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($notReadyEnvironment))
|
|
->resize(1236, 900)
|
|
->waitForText('Published with limitations')
|
|
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
|
->assertSee('Needs review')
|
|
->assertSee('Download review pack with limitations')
|
|
->assertSee('Review consumption flow')
|
|
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"]").length === 6', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Review pack\"]")?.dataset.stepState === "Available"', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Needs review"', true)
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true)
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider response should stay hidden')
|
|
->assertDontSee('stack trace should stay hidden')
|
|
->assertDontSee('spec342-browser-hidden-fingerprint')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$page->screenshot(true, spec342BrowserScreenshotName('01-evidence-incomplete-not-ready'));
|
|
spec342CopyBrowserScreenshot('01-evidence-incomplete-not-ready');
|
|
|
|
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($readyEnvironment))
|
|
->waitForText('Customer-safe review pack ready')
|
|
->assertSee('Stakeholders can use the current review pack and released review as the evidence path.')
|
|
->assertSee('Download customer-safe review pack')
|
|
->assertSee('Review pack state')
|
|
->assertSee('Export ready')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Spec342 Browser Operator')
|
|
->assertSee('No open findings require customer action.')
|
|
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-primary-action\"]").length === 1', true)
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-evidence-path-panel\"]")?.innerText.includes("Download review pack") === false', true)
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-secondary-action\"]")?.innerText.includes("Download customer-safe review pack") === false', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"]").length === 6', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Ready"', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"] [class*=\"badge\"], [data-testid=\"customer-review-review-pack-panel\"] [class*=\"badge\"], [data-testid=\"customer-review-accepted-risk-panel\"] [class*=\"badge\"]")).every((badge) => ! badge.innerText.includes("..."))', true)
|
|
->assertScript('document.body.innerHTML.includes("source_surface=customer_review_workspace")', true)
|
|
->assertScript('! document.body.innerHTML.includes("/admin/t/") && ! window.location.search.includes("tenant_id=")', true)
|
|
->assertDontSee('Auditor-ready')
|
|
->assertDontSee('environment is healthy')
|
|
->assertDontSee('compliant')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$page->screenshot(true, spec342BrowserScreenshotName('02-ready-with-evidence'));
|
|
spec342CopyBrowserScreenshot('02-ready-with-evidence');
|
|
|
|
$page->script('document.querySelector("[data-testid=\"customer-review-review-pack-panel\"]")?.scrollIntoView({ block: "center" });');
|
|
$page
|
|
->assertSee('Export availability')
|
|
->assertSee('Available');
|
|
$page->screenshot(true, spec342BrowserScreenshotName('03-review-pack-available'));
|
|
spec342CopyBrowserScreenshot('03-review-pack-available');
|
|
|
|
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($findingsEnvironment))
|
|
->waitForText('Findings needing attention')
|
|
->assertSee('Published with limitations')
|
|
->assertSee('1 open finding needs attention; 1 is high impact.')
|
|
->assertSee('Keep open findings visible before customer handoff.')
|
|
->assertSee('Do not treat this review as share-ready until open findings are resolved, accepted, or explicitly reviewed.')
|
|
->assertSee('High impact')
|
|
->assertSee('Open review')
|
|
->assertSee('Download review pack with limitations')
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-decision-card\"]")?.innerText.includes("Download review pack with limitations") === true', true)
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-evidence-path-panel\"]")?.innerText.includes("Download review pack") === false', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Findings triaged\"]")?.dataset.stepState === "Needs review"', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Findings triaged\"]")?.dataset.stepCurrent === "true"', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Needs review"', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$page->screenshot(true, spec342BrowserScreenshotName('04-findings-need-attention'));
|
|
spec342CopyBrowserScreenshot('04-findings-need-attention');
|
|
|
|
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acceptedRiskEnvironment))
|
|
->waitForText('Published with limitations')
|
|
->assertSee('Accepted-risk follow-up is recorded for this review. Review the owner, rationale, and review date before handoff.')
|
|
->assertSee('The pack can be shared only with the accepted-risk context included in the customer handoff.')
|
|
->assertSee('Open review')
|
|
->assertSee('Follow-up required')
|
|
->assertSee('Accepted-risk accountability')
|
|
->assertSee('Spec342 Browser Risk Owner')
|
|
->assertSee('Customer-approved maintenance window.')
|
|
->assertSee('Review date not recorded')
|
|
->assertSee('Accepted risk requires customer awareness.')
|
|
->assertSee('Download review pack with limitations')
|
|
->assertScript('document.querySelector("[data-testid=\"customer-review-decision-card\"]")?.innerText.includes("Download review pack with limitations") === true', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Accepted risks reviewed\"]")?.dataset.stepCurrent === "true"', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$page->screenshot(true, spec342BrowserScreenshotName('05-accepted-risks-present'));
|
|
spec342CopyBrowserScreenshot('05-accepted-risks-present');
|
|
|
|
$page->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true);
|
|
$page->screenshot(true, spec342BrowserScreenshotName('06-diagnostics-collapsed'));
|
|
spec342CopyBrowserScreenshot('06-diagnostics-collapsed');
|
|
|
|
$page->script("document.documentElement.classList.add('dark');");
|
|
$page->script('window.scrollTo(0, 0);');
|
|
$page->assertScript('document.documentElement.classList.contains("dark")', true);
|
|
$page->screenshot(true, spec342BrowserScreenshotName('07-dark-mode'));
|
|
spec342CopyBrowserScreenshot('07-dark-mode');
|
|
});
|
|
|
|
function spec342BrowserScreenshotName(string $name): string
|
|
{
|
|
return 'spec342-customer-review-workspace-'.$name;
|
|
}
|
|
|
|
function spec342CopyBrowserScreenshot(string $name): void
|
|
{
|
|
$filename = spec342BrowserScreenshotName($name).'.png';
|
|
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
|
$targetDirectory = repo_path('specs/342-customer-review-workspace-final-consumption-productization/artifacts/screenshots');
|
|
|
|
if (! is_dir($targetDirectory)) {
|
|
@mkdir($targetDirectory, 0755, true);
|
|
}
|
|
|
|
if (! is_file($source)) {
|
|
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
|
}
|
|
|
|
for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) {
|
|
usleep(100_000);
|
|
clearstatcache(true, $source);
|
|
}
|
|
|
|
if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) {
|
|
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png');
|
|
}
|
|
}
|
|
|
|
function spec342AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
|
|
{
|
|
$workspaceId = (int) $environment->workspace_id;
|
|
|
|
$test->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
|
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
]);
|
|
|
|
setAdminPanelContext($environment);
|
|
}
|
|
|
|
function spec342BrowserEnvironmentFor(User $user, ManagedEnvironment $baseEnvironment, string $name): ManagedEnvironment
|
|
{
|
|
$environment = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $baseEnvironment->workspace_id,
|
|
'name' => $name,
|
|
]);
|
|
|
|
createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager');
|
|
|
|
return $environment;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summaryOverrides
|
|
* @return array{0: EnvironmentReview, 1: ReviewPack}
|
|
*/
|
|
function spec342BrowserCreatePublishedReviewWithPack(
|
|
ManagedEnvironment $environment,
|
|
User $user,
|
|
EvidenceSnapshot $snapshot,
|
|
array $summaryOverrides = [],
|
|
string $filePath = 'review-packs/spec342-browser-review-pack.zip',
|
|
bool $normalizeOutputReadiness = true,
|
|
): array {
|
|
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
|
$summary = array_replace_recursive(
|
|
is_array($review->summary) ? $review->summary : [],
|
|
[
|
|
'control_interpretation' => [
|
|
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
|
'controls' => [
|
|
[
|
|
'control_key' => 'customer-output',
|
|
'title' => 'Customer output',
|
|
'readiness_bucket' => 'evidence_on_record',
|
|
'readiness_label' => 'Evidence on record',
|
|
'primary_reason' => 'Evidence path is complete.',
|
|
'recommended_next_action' => 'Open the current customer review pack.',
|
|
],
|
|
],
|
|
],
|
|
'governance_package' => [
|
|
'decision_summary' => [
|
|
'status' => 'none',
|
|
'evidence_state' => EnvironmentReviewCompletenessState::Complete->value,
|
|
'decision_data_state' => 'complete',
|
|
'total_count' => 0,
|
|
'summary' => 'No governance decisions require customer awareness.',
|
|
'next_action' => 'Open the current customer review pack.',
|
|
'entries' => [],
|
|
],
|
|
],
|
|
],
|
|
$summaryOverrides,
|
|
);
|
|
|
|
$run = OperationRun::factory()->forTenant($environment)->create([
|
|
'type' => OperationRunType::ReviewPackGenerate->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'started_at' => now()->subMinutes(6),
|
|
'completed_at' => now()->subMinutes(4),
|
|
'initiator_name' => 'Spec342 Browser Operator',
|
|
]);
|
|
|
|
Storage::disk('exports')->put($filePath, 'PK-spec342-browser-test');
|
|
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'completeness_state' => EnvironmentReviewCompletenessState::Complete->value,
|
|
'summary' => $summary,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'generated_at' => now()->subMinutes(5),
|
|
'published_at' => now()->subMinutes(3),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
if ($normalizeOutputReadiness) {
|
|
$review = markEnvironmentReviewCustomerSafeReady($review);
|
|
}
|
|
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'status' => ReviewPackStatus::Ready->value,
|
|
'options' => [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
],
|
|
'file_path' => $filePath,
|
|
'file_disk' => 'exports',
|
|
'generated_at' => now()->subMinutes(4),
|
|
]);
|
|
|
|
$review->forceFill([
|
|
'current_export_review_pack_id' => (int) $pack->getKey(),
|
|
])->save();
|
|
|
|
return [$review->refresh(), $pack->refresh()];
|
|
}
|
|
|
|
function spec342BrowserCreateAcceptedRisk(ManagedEnvironment $environment, User $user): void
|
|
{
|
|
$owner = User::factory()->create(['name' => 'Spec342 Browser Risk Owner']);
|
|
$findingWithReviewDate = Finding::factory()->riskAccepted()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
]);
|
|
$findingWithoutReviewDate = Finding::factory()->riskAccepted()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'finding_id' => (int) $findingWithReviewDate->getKey(),
|
|
'status' => FindingException::STATUS_ACTIVE,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'request_reason' => 'Customer-approved maintenance window.',
|
|
'owner_user_id' => (int) $owner->getKey(),
|
|
'approved_by_user_id' => (int) $owner->getKey(),
|
|
'requested_at' => now()->subDays(3),
|
|
'approved_at' => now()->subDays(2),
|
|
'effective_from' => now()->subDays(2),
|
|
'review_due_at' => now()->addDays(30),
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'finding_id' => (int) $findingWithoutReviewDate->getKey(),
|
|
'status' => FindingException::STATUS_ACTIVE,
|
|
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'request_reason' => 'Pending owner confirmation.',
|
|
'requested_at' => now()->subDay(),
|
|
'approved_at' => now()->subDay(),
|
|
'effective_from' => now()->subDay(),
|
|
'review_due_at' => null,
|
|
'expires_at' => null,
|
|
]);
|
|
}
|