Implemented the final operator workflow for the Governance Inbox. This includes refactoring the inbox page, updating finding resources, adding UI enforcement policies, updating related blade views, and adding comprehensive tests for operator workflow and scope contracts. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #418
462 lines
19 KiB
PHP
462 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Governance\GovernanceInbox;
|
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ManagedEnvironmentTriageReview;
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Support\BackupHealth\TenantBackupHealthResolver;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\PortfolioTriage\ManagedEnvironmentTriageReviewFingerprint;
|
|
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
it('Spec346 smokes the governance inbox summary-first hierarchy and lane scanability', function (): void {
|
|
[$user, $environmentA, $environmentB] = spec346GovernanceInboxBrowserFixture();
|
|
spec346AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
|
|
|
|
$page = visit(GovernanceInbox::getUrl(panel: 'admin'))
|
|
->resize(1440, 1100)
|
|
->waitForText('Governance Inbox')
|
|
->assertSee('Daily operator queue for governance follow-up, accepted risk, evidence gaps, and review handoff.')
|
|
->assertSee('Open governance work')
|
|
->assertSee('Next recommended action')
|
|
->assertSee('Triage Finding #')
|
|
->assertSee('Triage finding')
|
|
->assertSee('View lane')
|
|
->assertSee('Primary inbox lanes')
|
|
->assertSee('Needs triage')
|
|
->assertSee('Requires decision')
|
|
->assertSee('Risk / exception review')
|
|
->assertSee('Evidence required')
|
|
->assertSee('Blocked')
|
|
->assertSee('Recently resolved')
|
|
->assertSee('More context')
|
|
->assertSee('Source detail')
|
|
->assertDontSee('Primary next action')
|
|
->assertDontSee('Decision workbench')
|
|
->assertDontSee('Queue context')
|
|
->assertScript('document.querySelector("[data-testid=\"governance-inbox-diagnostics\"]")?.open === false', true)
|
|
->assertScript('document.querySelectorAll("[data-testid^=\"governance-inbox-item-\"] details[open]").length === 0', true)
|
|
->assertScript('(() => {
|
|
const summary = document.querySelector("[data-testid=\"governance-inbox-operator-summary\"]");
|
|
const nextAction = document.querySelector("[data-testid=\"governance-inbox-next-action\"]");
|
|
const lanes = document.querySelector("[data-testid=\"governance-inbox-lanes\"]");
|
|
const sourceDetail = document.querySelector("[data-testid=\"governance-inbox-source-detail\"]");
|
|
|
|
if (! summary || ! nextAction || ! lanes || ! sourceDetail) {
|
|
return false;
|
|
}
|
|
|
|
return summary.getBoundingClientRect().top < nextAction.getBoundingClientRect().top
|
|
&& nextAction.getBoundingClientRect().top < lanes.getBoundingClientRect().top
|
|
&& lanes.getBoundingClientRect().top < sourceDetail.getBoundingClientRect().top;
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const nextAction = document.querySelector("[data-testid=\"governance-inbox-next-action\"]");
|
|
const button = nextAction?.querySelector("a[href]");
|
|
|
|
if (! nextAction || ! button) {
|
|
return false;
|
|
}
|
|
|
|
const rect = nextAction.getBoundingClientRect();
|
|
const buttonRect = button.getBoundingClientRect();
|
|
|
|
return rect.top >= 0
|
|
&& rect.top < window.innerHeight
|
|
&& buttonRect.top < window.innerHeight
|
|
&& nextAction.textContent.includes("Next recommended action")
|
|
&& nextAction.textContent.includes("Triage Finding #");
|
|
})()', true)
|
|
->assertScript('document.documentElement.scrollWidth <= window.innerWidth', true)
|
|
->assertSee($environmentA->name)
|
|
->assertSee($environmentB->name)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$page->script('window.scrollTo(0, 0);');
|
|
$page->screenshot(true, spec346GovernanceInboxScreenshot('governance-inbox--summary-first'));
|
|
|
|
spec346CopyBrowserScreenshot('governance-inbox--summary-first');
|
|
|
|
$page->resize(390, 844);
|
|
$page->script('window.scrollTo(0, 0);');
|
|
$page
|
|
->assertScript('document.documentElement.scrollWidth <= window.innerWidth', true)
|
|
->assertScript('(() => {
|
|
const nextAction = document.querySelector("[data-testid=\"governance-inbox-next-action\"]");
|
|
const firstItem = document.querySelector("[data-testid^=\"governance-inbox-item-\"]");
|
|
|
|
if (! nextAction || ! firstItem) {
|
|
return false;
|
|
}
|
|
|
|
const nextRect = nextAction.getBoundingClientRect();
|
|
|
|
return nextRect.top >= 0
|
|
&& nextRect.top < window.innerHeight
|
|
&& nextAction.textContent.includes("Triage Finding #")
|
|
&& firstItem.textContent.includes("Reason")
|
|
&& firstItem.textContent.includes("Impact")
|
|
&& firstItem.textContent.includes("More context");
|
|
})()', true)
|
|
->screenshot(true, spec346GovernanceInboxScreenshot('governance-inbox--mobile-density'));
|
|
|
|
spec346CopyBrowserScreenshot('governance-inbox--mobile-density');
|
|
|
|
$hashPage = visit(GovernanceInbox::getUrl(panel: 'admin').'#lane-needs_triage')
|
|
->resize(1440, 1100)
|
|
->waitForText('Needs triage');
|
|
|
|
$hashPage->script('async () => { await new Promise((resolve) => setTimeout(resolve, 250)); return true; }');
|
|
$hashPage
|
|
->assertScript('(() => {
|
|
const lane = document.getElementById("lane-needs_triage");
|
|
|
|
if (! lane) {
|
|
return false;
|
|
}
|
|
|
|
const rect = lane.getBoundingClientRect();
|
|
|
|
return window.location.hash === "#lane-needs_triage"
|
|
&& window.scrollY > 0
|
|
&& rect.top >= 0
|
|
&& rect.top < Math.min(window.innerHeight * 0.35, 260);
|
|
})()', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec346GovernanceInboxScreenshot('governance-inbox--hash-lane-needs-triage'));
|
|
|
|
spec346CopyBrowserScreenshot('governance-inbox--hash-lane-needs-triage');
|
|
}
|
|
);
|
|
|
|
it('Spec346 smokes the filtered governance inbox URL contract', function (): void {
|
|
[$user, $environmentA, $environmentB] = spec346GovernanceInboxBrowserFixture();
|
|
spec346AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
|
|
|
|
$page = visit(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'environment_id' => (int) $environmentA->getKey(),
|
|
'family' => 'stale_operations',
|
|
]))
|
|
->waitForText('Environment filter:')
|
|
->assertSee('Environment filter: '.$environmentA->name)
|
|
->assertSee('Next recommended action')
|
|
->assertSee('Open terminal operation proof')
|
|
->assertSee('Blocked')
|
|
->assertDontSee($environmentB->name)
|
|
->assertScript('window.location.search.includes("environment_id='.(int) $environmentA->getKey().'")', true)
|
|
->assertScript('window.location.search.includes("family=stale_operations")', true)
|
|
->assertScript('! window.location.search.includes("tenant=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_id=")', true)
|
|
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_scope=")', true)
|
|
->assertScript('! window.location.search.includes("tableFilters")', true)
|
|
->assertScript('(() => {
|
|
const sourceDetail = document.querySelector("[data-testid=\"governance-inbox-source-detail\"]");
|
|
const activeFamily = sourceDetail?.querySelector("a[aria-current=\"page\"]");
|
|
|
|
return sourceDetail?.open === true
|
|
&& activeFamily?.textContent.includes("Operations follow-up")
|
|
&& activeFamily?.href.includes("family=stale_operations")
|
|
&& activeFamily?.hash === "#source-detail";
|
|
})()', true)
|
|
->assertScript('(() => {
|
|
const active = document.querySelector("[data-testid=\"governance-inbox-summary-active-counts\"]");
|
|
const clear = document.querySelector("[data-testid=\"governance-inbox-summary-clear-counts\"]");
|
|
|
|
if (! active || ! clear) {
|
|
return false;
|
|
}
|
|
|
|
return active.textContent.includes("Blocked")
|
|
&& clear.textContent.includes("Needs triage")
|
|
&& clear.textContent.includes("Requires decision")
|
|
&& clear.textContent.includes("Risk / exception review")
|
|
&& clear.textContent.includes("Evidence required")
|
|
&& (clear.textContent.match(/Clear/g) || []).length >= 4
|
|
&& document.querySelector("[data-testid=\"governance-inbox-lane-needs_triage\"]") === null
|
|
&& document.querySelector("[data-testid=\"governance-inbox-lane-blocked\"]") !== null;
|
|
})()', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$page->screenshot(true, spec346GovernanceInboxScreenshot('governance-inbox--filtered'));
|
|
|
|
spec346CopyBrowserScreenshot('governance-inbox--filtered');
|
|
}
|
|
);
|
|
|
|
it('Spec346 smokes a representative primary action hop and returns to the same inbox scope', function (): void {
|
|
[$user, $environmentA] = spec346GovernanceInboxBrowserFixture();
|
|
spec346AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
|
|
|
|
$expectedQueuePath = json_encode((string) parse_url(FindingExceptionsQueue::getUrl(panel: 'admin'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
|
|
|
$page = visit(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'environment_id' => (int) $environmentA->getKey(),
|
|
]))
|
|
->waitForText('Environment filter:')
|
|
->assertSee('Blocked')
|
|
->assertSee('Review accepted risk')
|
|
->assertScript('(() => {
|
|
const lane = document.querySelector("[data-testid=\"governance-inbox-lane-risk_exception_review\"]");
|
|
const link = [...(lane?.querySelectorAll("a[href]") || [])]
|
|
.find((element) => element.textContent.includes("Review accepted risk"));
|
|
|
|
return Boolean(link);
|
|
})()', true);
|
|
|
|
$page->script('(() => {
|
|
const lane = document.querySelector("[data-testid=\"governance-inbox-lane-risk_exception_review\"]");
|
|
const link = [...(lane?.querySelectorAll("a[href]") || [])]
|
|
.find((element) => element.textContent.includes("Review accepted risk"));
|
|
|
|
link?.click();
|
|
})()');
|
|
|
|
$page
|
|
->waitForText('Finding Exceptions Queue')
|
|
->assertScript("window.location.pathname === {$expectedQueuePath}", true)
|
|
->assertScript('window.location.search.includes("environment_id='.(int) $environmentA->getKey().'")', true)
|
|
->assertScript('window.location.search.includes("exception=")', true)
|
|
->assertScript('! window.location.search.includes("tenant=")', true)
|
|
->assertScript('! window.location.search.includes("tenant_id=")', true)
|
|
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$page->script('window.history.back();');
|
|
|
|
$page
|
|
->waitForText('Governance Inbox')
|
|
->waitForText('Environment filter: '.$environmentA->name)
|
|
->assertSee('Blocked')
|
|
->assertSee('Environment filter: '.$environmentA->name)
|
|
->assertScript('window.location.search.includes("environment_id='.(int) $environmentA->getKey().'")', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec346GovernanceInboxScreenshot('governance-inbox--return-scope'));
|
|
|
|
spec346CopyBrowserScreenshot('governance-inbox--return-scope');
|
|
}
|
|
);
|
|
|
|
/**
|
|
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
|
|
*/
|
|
function spec346GovernanceInboxBrowserFixture(): array
|
|
{
|
|
$environmentA = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec346 Browser Environment A',
|
|
'external_id' => 'spec346-browser-environment-a',
|
|
]);
|
|
[$user, $environmentA] = createUserWithTenant(
|
|
tenant: $environmentA,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
);
|
|
|
|
$environmentB = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'name' => 'Spec346 Browser Environment B',
|
|
'external_id' => 'spec346-browser-environment-b',
|
|
]);
|
|
createUserWithTenant(
|
|
tenant: $environmentB,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
);
|
|
|
|
Finding::factory()
|
|
->for($environmentA)
|
|
->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'status' => Finding::STATUS_NEW,
|
|
'owner_user_id' => null,
|
|
'assignee_user_id' => null,
|
|
'evidence_jsonb' => [],
|
|
]);
|
|
|
|
Finding::factory()
|
|
->for($environmentA)
|
|
->assignedTo((int) $user->getKey())
|
|
->ownedBy((int) $user->getKey())
|
|
->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'status' => Finding::STATUS_IN_PROGRESS,
|
|
'evidence_jsonb' => [],
|
|
]);
|
|
|
|
Finding::factory()
|
|
->for($environmentB)
|
|
->assignedTo((int) $user->getKey())
|
|
->ownedBy((int) $user->getKey())
|
|
->create([
|
|
'workspace_id' => (int) $environmentB->workspace_id,
|
|
'status' => Finding::STATUS_IN_PROGRESS,
|
|
'evidence_jsonb' => [
|
|
'summary' => [
|
|
'kind' => 'policy_snapshot',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$exceptionFinding = Finding::factory()
|
|
->for($environmentA)
|
|
->riskAccepted()
|
|
->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'finding_id' => (int) $exceptionFinding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
'request_reason' => 'Spec346 browser accepted-risk support review',
|
|
'requested_at' => now()->subDay(),
|
|
'review_due_at' => now()->addDays(3),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
OperationRun::factory()
|
|
->forTenant($environmentA)
|
|
->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$backupHealthResolver = app(TenantBackupHealthResolver::class);
|
|
$fingerprints = app(ManagedEnvironmentTriageReviewFingerprint::class);
|
|
$fingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($environmentA));
|
|
|
|
ManagedEnvironmentTriageReview::factory()
|
|
->for($environmentA)
|
|
->followUpNeeded()
|
|
->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'reviewed_by_user_id' => (int) $user->getKey(),
|
|
'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
|
|
'review_fingerprint' => $fingerprint['fingerprint'],
|
|
'review_snapshot' => $fingerprint['snapshot'],
|
|
]);
|
|
|
|
spec346SeedBrowserRecentlyClosedDecision($environmentA, $user);
|
|
|
|
return [$user, $environmentA, $environmentB];
|
|
}
|
|
|
|
function spec346AuthenticateGovernanceInboxBrowser(
|
|
mixed $test,
|
|
User $user,
|
|
ManagedEnvironment $rememberedEnvironment,
|
|
): void {
|
|
$workspaceId = (int) $rememberedEnvironment->workspace_id;
|
|
|
|
$session = [
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
|
|
],
|
|
];
|
|
|
|
$test->actingAs($user)->withSession($session);
|
|
|
|
foreach ($session as $key => $value) {
|
|
session()->put($key, $value);
|
|
}
|
|
|
|
setAdminPanelContext($rememberedEnvironment);
|
|
}
|
|
|
|
function spec346GovernanceInboxScreenshot(string $name): string
|
|
{
|
|
return 'spec346-'.$name;
|
|
}
|
|
|
|
function spec346CopyBrowserScreenshot(string $name, ?string $targetFilename = null): void
|
|
{
|
|
$filename = spec346GovernanceInboxScreenshot($name).'.png';
|
|
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
|
$targetDirectory = repo_path('specs/346-governance-inbox-final-operator-workflow/artifacts/screenshots');
|
|
$targetFilename ??= $filename;
|
|
|
|
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_dir($targetDirectory)) {
|
|
@mkdir($targetDirectory, 0755, true);
|
|
}
|
|
|
|
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
|
|
return;
|
|
}
|
|
|
|
if (is_file($source)) {
|
|
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename);
|
|
}
|
|
}
|
|
|
|
function spec346SeedBrowserRecentlyClosedDecision(ManagedEnvironment $environment, User $user): void
|
|
{
|
|
$finding = Finding::factory()
|
|
->for($environment)
|
|
->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'status' => Finding::STATUS_IN_PROGRESS,
|
|
]);
|
|
|
|
$exception = FindingException::query()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_REJECTED,
|
|
'current_validity_state' => FindingException::VALIDITY_REJECTED,
|
|
'request_reason' => 'Browser recently closed decision',
|
|
'requested_at' => now()->subDays(5),
|
|
'review_due_at' => now()->subDays(2),
|
|
'rejected_at' => now()->subDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
$decision = FindingExceptionDecision::query()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'finding_exception_id' => (int) $exception->getKey(),
|
|
'actor_user_id' => (int) $user->getKey(),
|
|
'decision_type' => FindingExceptionDecision::TYPE_REJECTED,
|
|
'reason' => 'Browser recently rejected closure reason',
|
|
'decided_at' => now()->subDay(),
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$exception->forceFill([
|
|
'current_decision_id' => (int) $decision->getKey(),
|
|
])->save();
|
|
}
|