browser()->timeout(60_000); it('Spec328 smokes non-empty operations hub decision workbench entry', function (): void { [$user, $environmentA, $environmentB] = spec328OperationsHubFixture(); spec328AuthenticateOperationsHubBrowser($this, $user, $environmentA); $page = visit(OperationRunLinks::index(workspace: $environmentA->workspace)) ->resize(1440, 1100) ->waitForText('Operations Hub') ->assertDontSee(__('localization.shell.no_environment_selected')) ->assertDontSee('Environment filter:') ->assertSee('Execution follow-up workbench') ->assertSee('Which operation needs attention now?') ->assertSee('Decision workbench') ->assertSee('Needs attention') ->assertSee('Active operations') ->assertSee('Failed or blocked') ->assertSee('Completed recently') ->assertDontSee('Total Operations') ->assertDontSee('Avg Duration') ->assertDontSee('sparkline') ->assertDontSee('trend') ->assertSee('Inventory sync') ->assertSee('Outcome') ->assertSee('Blocked') ->assertSee('Reason') ->assertSee('Impact') ->assertSee($environmentA->name) ->assertSee('Proof') ->assertSee('Operation detail available') ->assertSee('Primary next action') ->assertSee('Open operation') ->assertSee('Operations history') ->assertSee('Policy sync') ->assertDontSee('tenant filter') ->assertDontSee('current tenant') ->assertDontSee('entitled tenant') ->assertDontSee('all tenants') ->assertDontSee('raw payload should stay hidden') ->assertDontSee('stack trace should stay hidden') ->assertDontSee('provider secret should stay hidden') ->assertDontSee('debug metadata should stay hidden') ->assertDontSee('internal exception should stay hidden') ->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true) ->assertScript('(() => { const summaryCards = document.querySelector("[data-testid=\"operations-hub-summary-cards\"]"); const nativeStats = summaryCards?.querySelector(".fi-wi-stats-overview"); const needsAttention = document.querySelector("[data-testid=\"operations-workbench-stat-needs-attention\"]"); const activeOperations = document.querySelector("[data-testid=\"operations-workbench-stat-active-operations\"]"); const failedOrBlocked = document.querySelector("[data-testid=\"operations-workbench-stat-failed-or-blocked\"]"); const completedRecently = document.querySelector("[data-testid=\"operations-workbench-stat-completed-recently\"]"); if (! summaryCards || ! nativeStats || ! needsAttention || ! activeOperations || ! failedOrBlocked || ! completedRecently) { return false; } if ( needsAttention.dataset.statColor !== "warning" || needsAttention.dataset.statValue !== "5" || activeOperations.dataset.statColor !== "info" || activeOperations.dataset.statValue !== "0" || failedOrBlocked.dataset.statColor !== "danger" || failedOrBlocked.dataset.statValue !== "5" || completedRecently.dataset.statColor !== "success" || completedRecently.dataset.statValue !== "0" ) { return false; } if (summaryCards.textContent.includes("Total Operations") || summaryCards.textContent.includes("Avg Duration")) { return false; } if ( summaryCards.querySelector("canvas") || summaryCards.querySelector("[data-card-style]") || summaryCards.querySelector("[data-accent-placement]") || summaryCards.outerHTML.includes("wire:poll") ) { return false; } const decisionGrid = document.querySelector("[data-testid=\"operations-hub-decision-workbench\"]"); const workbench = document.querySelector("[data-testid=\"operations-hub-priority-card\"]"); const detail = document.querySelector("[data-testid=\"operations-hub-operation-proof-panel\"]"); if (! decisionGrid || ! workbench || ! detail) { return false; } const children = Array.from(decisionGrid.children); const summaryBox = summaryCards.getBoundingClientRect(); const decisionBox = decisionGrid.getBoundingClientRect(); const workbenchBox = workbench.getBoundingClientRect(); const detailBox = detail.getBoundingClientRect(); return window.innerWidth >= 1024 && summaryBox.bottom <= decisionBox.top && decisionGrid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]") && detail.tagName === "ASIDE" && children.indexOf(workbench) !== -1 && children.indexOf(detail) > children.indexOf(workbench) && detailBox.left > workbenchBox.right && Math.abs(detailBox.top - workbenchBox.top) <= 8; })()', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $page ->click('[data-testid="operations-hub-diagnostics"] summary') ->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === true', true) ->assertSee('Raw context, provider payloads, stack traces, debug metadata, and support diagnostics stay on authorized operation detail surfaces.') ->click('[data-testid="operations-hub-diagnostics"] summary') ->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $page->script('window.scrollTo(0, 0);'); $page ->screenshot(false, spec328OperationsHubScreenshot('operations-hub-premium-summary-cards')) ->screenshot(true, spec328OperationsHubScreenshot('operations-hub--clean')); spec328CopyBrowserScreenshot('operations-hub--clean'); spec328CopyBrowserScreenshot('operations-hub--clean', 'operations-hub-decision-workbench.png'); spec328CopyBrowserScreenshot('operations-hub-premium-summary-cards'); }); it('Spec328 smokes filtered operations hub clear and reload behavior', function (): void { [$user, $environmentA, $environmentB] = spec328OperationsHubFixture(); $cleanPath = json_encode((string) parse_url(OperationRunLinks::index(workspace: $environmentA->workspace), PHP_URL_PATH), JSON_THROW_ON_ERROR); spec328AuthenticateOperationsHubBrowser($this, $user, $environmentA); $page = visit(OperationRunLinks::index($environmentA)) ->waitForText('Environment filter:') ->assertSee('Environment filter: '.$environmentA->name) ->assertSee('Which operation needs attention now?') ->assertSee($environmentA->name) ->assertDontSee('Policy sync') ->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->screenshot(true, spec328OperationsHubScreenshot('operations-hub--filtered')); spec328CopyBrowserScreenshot('operations-hub--filtered'); spec328ClearEnvironmentFilter($page) ->waitForText('Policy sync') ->assertDontSee('Environment filter:') ->assertSee('Policy sync') ->assertScript("window.location.pathname === {$cleanPath}", true) ->assertScript('! window.location.search.includes("environment_id=")', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->screenshot(true, spec328OperationsHubScreenshot('operations-hub--after-clear')); spec328CopyBrowserScreenshot('operations-hub--after-clear'); $page->script('window.location.reload();'); $page ->waitForText('Policy sync') ->assertDontSee('Environment filter:') ->assertSee('Policy sync') ->assertScript("window.location.pathname === {$cleanPath}", true) ->assertScript('! window.location.search.includes("environment_id=")', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->screenshot(true, spec328OperationsHubScreenshot('operations-hub--after-reload')); spec328CopyBrowserScreenshot('operations-hub--after-reload'); }); it('Spec328 smokes no-attention operations hub state', function (): void { $environment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec328 Browser Empty Environment', 'external_id' => 'spec328-browser-empty-environment', ]); [$user, $environment] = createUserWithTenant( tenant: $environment, role: 'owner', workspaceRole: 'owner', ); OperationRun::factory()->forTenant($environment)->create([ 'type' => 'policy.sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'completed_at' => now()->subMinutes(5), ]); spec328AuthenticateOperationsHubBrowser($this, $user, $environment); visit(OperationRunLinks::index(workspace: $environment->workspace)) ->waitForText('Operations Hub') ->assertSee('No operations need attention') ->assertSee('This is execution follow-up only, not an environment health claim.') ->assertSee('Operation detail available') ->assertDontSee('environment is healthy') ->assertDontSee('governance health is complete') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->screenshot(true, spec328OperationsHubScreenshot('operations-hub--empty')); spec328CopyBrowserScreenshot('operations-hub--empty'); }); /** * @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment} */ function spec328OperationsHubFixture(): array { $environmentA = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec328 Browser Environment A', 'external_id' => 'spec328-browser-environment-a', ]); [$user, $environmentA] = createUserWithTenant( tenant: $environmentA, role: 'owner', workspaceRole: 'owner', ); $environmentB = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $environmentA->workspace_id, 'name' => 'Spec328 Browser Environment B', 'external_id' => 'spec328-browser-environment-b', ]); createUserWithTenant( tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner', ); OperationRun::factory()->forTenant($environmentA)->create([ 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Blocked->value, 'context' => [ 'reason_code' => 'write_gate_blocked', 'raw_payload' => 'raw payload should stay hidden', 'stack_trace' => 'stack trace should stay hidden', 'provider_secret' => 'provider secret should stay hidden', 'debug_metadata' => 'debug metadata should stay hidden', 'internal_exception' => 'internal exception should stay hidden', ], 'completed_at' => null, ]); foreach (range(1, 4) as $index) { OperationRun::factory()->forTenant($environmentA)->create([ 'type' => 'backup.schedule.execute', 'status' => OperationRunStatus::Completed->value, 'outcome' => $index % 2 === 0 ? OperationRunOutcome::Blocked->value : OperationRunOutcome::Failed->value, 'completed_at' => null, ]); } OperationRun::factory()->forTenant($environmentB)->create([ 'type' => 'policy.sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'completed_at' => now()->subDays(2), ]); return [$user, $environmentA, $environmentB]; } function spec328AuthenticateOperationsHubBrowser( 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 spec328ClearEnvironmentFilter(mixed $page): mixed { $page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true); $page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);'); return $page; } function spec328OperationsHubScreenshot(string $name): string { return 'spec328-'.$name; } function spec328CopyBrowserScreenshot(string $name, ?string $targetFilename = null): void { $filename = spec328OperationsHubScreenshot($name).'.png'; $source = base_path('tests/Browser/Screenshots/'.$filename); $targetDirectory = repo_path('specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots'); $targetFilename ??= $filename; if (! is_dir($targetDirectory)) { @mkdir($targetDirectory, 0755, true); } if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) { return; } if (! is_file($source)) { $source = \Pest\Browser\Support\Screenshot::path($filename); } if (is_file($source)) { @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename); } }