From 12a812825f4aae84e1d6926df71503c13a1f36d6 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 2 Apr 2026 02:36:01 +0200 Subject: [PATCH] feat: retrofit deferred operator surfaces --- .github/agents/copilot-instructions.md | 3 +- .../ManagedTenantOnboardingWizard.php | 136 +++++++- .../TenantResource/Pages/ViewTenant.php | 12 +- .../Tenant/RecentOperationsSummary.php | 5 + .../Tenant/TenantVerificationReport.php | 7 +- app/Support/OperationRunLinks.php | 15 + .../ActionSurface/ActionSurfaceExemptions.php | 2 +- ...t-onboarding-verification-report.blade.php | 161 ++++----- ...g-verification-technical-details.blade.php | 54 ++- .../recent-operations-summary.blade.php | 26 +- .../tenant-verification-report.blade.php | 85 +---- .../checklists/requirements.md | 3 +- .../embedded-operation-surface-contract.yaml | 138 ++++++++ .../data-model.md | 180 ++++++++++ .../plan.md | 134 ++++++++ .../quickstart.md | 85 +++++ .../research.md | 49 +++ .../spec.md | 59 ++-- .../tasks.md | 190 +++++++++++ .../OnboardingDraftVerificationResumeTest.php | 4 +- ...ec172DeferredOperatorSurfacesSmokeTest.php | 318 ++++++++++++++++++ .../RecentOperationsSummaryWidgetTest.php | 3 + .../TenantVerificationReportWidgetTest.php | 29 ++ .../Guards/ActionSurfaceContractTest.php | 18 +- .../OnboardingVerificationClustersTest.php | 95 +++++- .../Onboarding/OnboardingVerificationTest.php | 109 +++++- .../OnboardingVerificationV1_5UxTest.php | 31 ++ 27 files changed, 1716 insertions(+), 235 deletions(-) create mode 100644 specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml create mode 100644 specs/172-deferred-operator-surfaces-retrofit/data-model.md create mode 100644 specs/172-deferred-operator-surfaces-retrofit/plan.md create mode 100644 specs/172-deferred-operator-surfaces-retrofit/quickstart.md create mode 100644 specs/172-deferred-operator-surfaces-retrofit/research.md create mode 100644 specs/172-deferred-operator-surfaces-retrofit/tasks.md create mode 100644 tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index 045e32bd..7a4306c4 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -122,6 +122,7 @@ ## Active Technologies - PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes (170-system-operations-surface-alignment) - PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` (171-operations-naming-consolidation) - PostgreSQL with existing `operation_runs`, notification payloads, workspace records, and tenant records; no schema changes (171-operations-naming-consolidation) +- PostgreSQL with existing `operation_runs`, `managed_tenant_onboarding_sessions`, tenant records, and workspace records; no schema changes (172-deferred-operator-surfaces-retrofit) - PHP 8.4.15 (feat/005-bulk-operations) @@ -141,8 +142,8 @@ ## Code Style PHP 8.4.15: Follow standard conventions ## Recent Changes +- 172-deferred-operator-surfaces-retrofit: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` - 171-operations-naming-consolidation: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` - 170-system-operations-surface-alignment: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` -- 169-action-surface-v11: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs diff --git a/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php b/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php index cc91d881..7e38eee5 100644 --- a/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php +++ b/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php @@ -87,6 +87,7 @@ use Illuminate\Database\QueryException; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; use Illuminate\Validation\ValidationException; use InvalidArgumentException; use Livewire\Attributes\Locked; @@ -564,7 +565,7 @@ public function content(Schema $schema): Schema ->default(null) ->view('filament.forms.components.managed-tenant-onboarding-verification-report') ->viewData(fn (): array => $this->verificationReportViewData()) - ->visible(fn (): bool => $this->verificationRunUrl() !== null), + ->visible(fn (): bool => $this->managedTenant instanceof Tenant), ]), ]) ->beforeValidation(function (): void { @@ -1708,27 +1709,24 @@ private function verificationStatusColor(): string private function verificationRunUrl(): ?string { - if (! $this->managedTenant instanceof Tenant) { + $run = $this->verificationRun(); + + if (! $run instanceof OperationRun) { return null; } - if (! $this->onboardingSession instanceof TenantOnboardingSession) { + if (! $this->canInspectOperationRun($run)) { return null; } - $runId = $this->onboardingSession->state['verification_operation_run_id'] ?? null; - - if (! is_int($runId)) { - return null; - } - - return $this->tenantlessOperationRunUrl($runId); + return $this->tenantlessOperationRunUrl((int) $run->getKey()); } /** * @return array{ * run: array|null, * runUrl: string|null, + * advancedRunUrl: string|null, * report: array|null, * fingerprint: string|null, * changeIndicator: array{state: 'no_changes'|'changed', previous_report_id: int}|null, @@ -1742,7 +1740,9 @@ private function verificationRunUrl(): ?string * acknowledged_by: array{id: int, name: string}|null * }>, * assistVisibility: array{is_visible: bool, reason: 'permission_blocked'|'permission_attention'|'hidden_ready'|'hidden_irrelevant'}, - * assistActionName: string + * assistActionName: string, + * technicalDetailsActionName: string, + * runState: 'no_run'|'active'|'completed' * } */ private function verificationReportViewData(): array @@ -1755,6 +1755,7 @@ private function verificationReportViewData(): array return [ 'run' => null, 'runUrl' => $runUrl, + 'advancedRunUrl' => null, 'report' => null, 'fingerprint' => null, 'changeIndicator' => null, @@ -1763,6 +1764,8 @@ private function verificationReportViewData(): array 'acknowledgements' => [], 'assistVisibility' => $assistVisibility, 'assistActionName' => 'wizardVerificationRequiredPermissionsAssist', + 'technicalDetailsActionName' => 'wizardVerificationTechnicalDetails', + 'runState' => 'no_run', ]; } @@ -1770,9 +1773,7 @@ private function verificationReportViewData(): array $fingerprint = is_array($report) ? VerificationReportViewer::fingerprint($report) : null; $changeIndicator = VerificationReportChangeIndicator::forRun($run); - $previousRunUrl = $changeIndicator === null - ? null - : $this->tenantlessOperationRunUrl((int) $changeIndicator['previous_report_id']); + $previousRunUrl = $this->verificationPreviousRunUrl($changeIndicator); $user = auth()->user(); $canAcknowledge = $user instanceof User && $this->managedTenant instanceof Tenant @@ -1820,11 +1821,13 @@ private function verificationReportViewData(): array 'outcome' => (string) $run->outcome, 'initiator_name' => (string) $run->initiator_name, 'started_at' => $run->started_at?->toJSON(), + 'updated_at' => $run->updated_at?->toJSON(), 'completed_at' => $run->completed_at?->toJSON(), 'target_scope' => $targetScope, 'failures' => $failures, ], 'runUrl' => $runUrl, + 'advancedRunUrl' => $runUrl, 'report' => $report, 'fingerprint' => $fingerprint, 'changeIndicator' => $changeIndicator, @@ -1833,6 +1836,8 @@ private function verificationReportViewData(): array 'acknowledgements' => $acknowledgements, 'assistVisibility' => $assistVisibility, 'assistActionName' => 'wizardVerificationRequiredPermissionsAssist', + 'technicalDetailsActionName' => 'wizardVerificationTechnicalDetails', + 'runState' => (string) $run->status === OperationRunStatus::Completed->value ? 'completed' : 'active', ]; } @@ -1855,6 +1860,26 @@ public function wizardVerificationRequiredPermissionsAssistAction(): Action ->visible(fn (): bool => $this->verificationAssistVisibility()['is_visible']); } + public function wizardVerificationTechnicalDetailsAction(): Action + { + return Action::make('wizardVerificationTechnicalDetails') + ->label('Technical details') + ->icon('heroicon-m-information-circle') + ->color('gray') + ->modal() + ->slideOver() + ->stickyModalHeader() + ->modalHeading('Verification technical details') + ->modalDescription('Diagnostics-only details for the current verification run.') + ->modalSubmitAction(false) + ->modalCancelAction(fn (Action $action): Action => $action->label('Close')) + ->modalContent(fn (): View => view( + 'filament.modals.onboarding-verification-technical-details', + $this->verificationTechnicalDetailsViewData(), + )) + ->visible(fn (): bool => $this->verificationRun() instanceof OperationRun); + } + public function acknowledgeVerificationCheckAction(): Action { return Action::make('acknowledgeVerificationCheck') @@ -3229,6 +3254,89 @@ private function tenantlessOperationRunUrl(int $runId): string return OperationRunLinks::tenantlessView($runId); } + /** + * @param array{state: 'no_changes'|'changed', previous_report_id: int}|null $changeIndicator + */ + private function verificationPreviousRunUrl(?array $changeIndicator): ?string + { + if (! is_array($changeIndicator)) { + return null; + } + + $previousRunId = $changeIndicator['previous_report_id'] ?? null; + + if (! is_int($previousRunId)) { + return null; + } + + $previousRun = OperationRun::query()->whereKey($previousRunId)->first(); + + if (! $previousRun instanceof OperationRun) { + return null; + } + + if (! $this->canInspectOperationRun($previousRun)) { + return null; + } + + return $this->tenantlessOperationRunUrl((int) $previousRun->getKey()); + } + + /** + * @return array{ + * run: array|null, + * runUrl: string|null, + * previousRunUrl: string|null, + * hasReport: bool + * } + */ + private function verificationTechnicalDetailsViewData(): array + { + $run = $this->verificationRun(); + + if (! $run instanceof OperationRun) { + return [ + 'run' => null, + 'runUrl' => null, + 'previousRunUrl' => null, + 'hasReport' => false, + ]; + } + + $report = VerificationReportViewer::report($run); + $changeIndicator = VerificationReportChangeIndicator::forRun($run); + $context = is_array($run->context ?? null) ? $run->context : []; + $targetScope = $context['target_scope'] ?? []; + $targetScope = is_array($targetScope) ? $targetScope : []; + + return [ + 'run' => [ + 'id' => (int) $run->getKey(), + 'type' => (string) $run->type, + 'status' => (string) $run->status, + 'outcome' => (string) $run->outcome, + 'started_at' => $run->started_at?->toJSON(), + 'updated_at' => $run->updated_at?->toJSON(), + 'completed_at' => $run->completed_at?->toJSON(), + 'target_scope' => $targetScope, + ], + 'runUrl' => $this->verificationRunUrl(), + 'previousRunUrl' => $this->verificationPreviousRunUrl($changeIndicator), + 'hasReport' => is_array($report), + ]; + } + + private function canInspectOperationRun(OperationRun $run): bool + { + $user = auth()->user(); + + if (! $user instanceof User) { + return false; + } + + return Gate::forUser($user)->allows('view', $run); + } + public function verificationSucceeded(): bool { return $this->verificationHasSucceeded(); diff --git a/app/Filament/Resources/TenantResource/Pages/ViewTenant.php b/app/Filament/Resources/TenantResource/Pages/ViewTenant.php index 65498913..04d4a90a 100644 --- a/app/Filament/Resources/TenantResource/Pages/ViewTenant.php +++ b/app/Filament/Resources/TenantResource/Pages/ViewTenant.php @@ -29,6 +29,16 @@ class ViewTenant extends ViewRecord { protected static string $resource = TenantResource::class; + public static function verificationHeaderActionLabel(): string + { + return 'Verify configuration'; + } + + public static function verificationHeaderActionHint(): string + { + return 'Use "'.self::verificationHeaderActionLabel().'" in the tenant header to run verification again after you inspect the current operation.'; + } + public function getHeaderWidgetsColumns(): int|array { return 1; @@ -83,7 +93,7 @@ protected function getHeaderActions(): array ->openUrlInNewTab(), UiEnforcement::forAction( Actions\Action::make('verify') - ->label('Verify configuration') + ->label(self::verificationHeaderActionLabel()) ->icon('heroicon-o-check-badge') ->color('primary') ->requiresConfirmation() diff --git a/app/Filament/Widgets/Tenant/RecentOperationsSummary.php b/app/Filament/Widgets/Tenant/RecentOperationsSummary.php index 96c8a321..7389aa91 100644 --- a/app/Filament/Widgets/Tenant/RecentOperationsSummary.php +++ b/app/Filament/Widgets/Tenant/RecentOperationsSummary.php @@ -6,6 +6,7 @@ use App\Models\OperationRun; use App\Models\Tenant; +use App\Support\OperationRunLinks; use Filament\Facades\Filament; use Filament\Widgets\Widget; use Illuminate\Database\Eloquent\Collection; @@ -41,6 +42,8 @@ protected function getViewData(): array 'tenant' => null, 'runs' => collect(), 'operationsIndexUrl' => route('admin.operations.index'), + 'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(), + 'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(), ]; } @@ -64,6 +67,8 @@ protected function getViewData(): array 'tenant' => $tenant, 'runs' => $runs, 'operationsIndexUrl' => route('admin.operations.index'), + 'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(), + 'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(), ]; } } diff --git a/app/Filament/Widgets/Tenant/TenantVerificationReport.php b/app/Filament/Widgets/Tenant/TenantVerificationReport.php index c04c3e91..f46d0783 100644 --- a/app/Filament/Widgets/Tenant/TenantVerificationReport.php +++ b/app/Filament/Widgets/Tenant/TenantVerificationReport.php @@ -4,6 +4,7 @@ namespace App\Filament\Widgets\Tenant; +use App\Filament\Resources\TenantResource\Pages\ViewTenant; use App\Filament\Support\VerificationReportViewer; use App\Models\OperationRun; use App\Models\Tenant; @@ -175,6 +176,7 @@ protected function getViewData(): array 'isInProgress' => false, 'canStart' => false, 'startTooltip' => null, + 'rerunHint' => null, ]; } @@ -230,10 +232,13 @@ protected function getViewData(): array 'report' => $report, 'redactionNotes' => VerificationReportViewer::redactionNotes($report), 'isInProgress' => $isInProgress, - 'showStartAction' => $isTenantMember && $canOperate, + 'showStartAction' => ! ($run instanceof OperationRun) && $isTenantMember && $canOperate, 'canStart' => $canStart, 'startTooltip' => $isTenantMember && $canOperate && ! $canStart ? UiTooltips::insufficientPermission() : null, 'lifecycleNotice' => $lifecycleNotice, + 'rerunHint' => $run instanceof OperationRun && $canStart + ? ViewTenant::verificationHeaderActionHint() + : null, ]; } } diff --git a/app/Support/OperationRunLinks.php b/app/Support/OperationRunLinks.php index 1e6e26bf..55eae9ad 100644 --- a/app/Support/OperationRunLinks.php +++ b/app/Support/OperationRunLinks.php @@ -33,6 +33,11 @@ public static function openCollectionLabel(): string return 'Open operations'; } + public static function collectionScopeDescription(): string + { + return 'Broader admin view across recent and historical operations.'; + } + public static function viewInCollectionLabel(): string { return 'View in Operations'; @@ -48,6 +53,16 @@ public static function openLabel(): string return 'Open operation'; } + public static function advancedMonitoringLabel(): string + { + return 'Open operation in Monitoring (advanced)'; + } + + public static function advancedMonitoringDescription(): string + { + return 'Diagnostics-only link to the canonical admin operation viewer.'; + } + public static function identifierLabel(): string { return 'Operation ID'; diff --git a/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php b/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php index 5e843ac5..623bac60 100644 --- a/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php +++ b/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php @@ -27,7 +27,7 @@ public static function baseline(): self 'App\\Filament\\Pages\\Monitoring\\Alerts' => 'Monitoring alerts remains exempt because the active admin alerts surface resolves through the cluster entry at /admin/alerts, not this page-class route.', 'App\\Filament\\Pages\\Tenancy\\RegisterTenant' => 'Tenant onboarding route is covered by onboarding/RBAC specs.', 'App\\Filament\\Pages\\TenantDashboard' => 'Dashboard retrofit deferred; widget and summary surfaces are excluded from this contract.', - 'App\\Filament\\Pages\\Workspaces\\ManagedTenantOnboardingWizard' => 'Onboarding wizard has dedicated conformance tests and remains exempt in spec 082.', + 'App\\Filament\\Pages\\Workspaces\\ManagedTenantOnboardingWizard' => 'Onboarding wizard has dedicated conformance tests in spec 172 (OnboardingVerificationTest, OnboardingVerificationClustersTest, OnboardingVerificationV1_5UxTest) and remains exempt from blanket discovery.', 'App\\Filament\\Pages\\Workspaces\\ManagedTenantsLanding' => 'Managed-tenant landing retrofit deferred to workspace feature track.', ], TenantOwnedModelFamilies::actionSurfaceBaselineExemptions())); } diff --git a/resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php b/resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php index d883c260..a0c285ea 100644 --- a/resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php +++ b/resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php @@ -19,6 +19,9 @@ $previousRunUrl = $previousRunUrl ?? null; $previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null; + $advancedRunUrl = $advancedRunUrl ?? null; + $advancedRunUrl = is_string($advancedRunUrl) && $advancedRunUrl !== '' ? $advancedRunUrl : null; + $canAcknowledge = (bool) ($canAcknowledge ?? false); $acknowledgements = $acknowledgements ?? []; @@ -32,6 +35,11 @@ ? trim($assistActionName) : 'wizardVerificationRequiredPermissionsAssist'; + $technicalDetailsActionName = $technicalDetailsActionName ?? 'wizardVerificationTechnicalDetails'; + $technicalDetailsActionName = is_string($technicalDetailsActionName) && trim($technicalDetailsActionName) !== '' + ? trim($technicalDetailsActionName) + : 'wizardVerificationTechnicalDetails'; + $showAssist = (bool) ($assistVisibility['is_visible'] ?? false); $assistReason = $assistVisibility['reason'] ?? 'hidden_irrelevant'; $assistReason = is_string($assistReason) ? $assistReason : 'hidden_irrelevant'; @@ -149,6 +157,15 @@ } $linkBehavior = app(\App\Support\Verification\VerificationLinkBehavior::class); + + $runState = $runState ?? null; + $runState = is_string($runState) ? $runState : null; + + if (! in_array($runState, ['no_run', 'active', 'completed'], true)) { + $runState = $run === null + ? 'no_run' + : ($status === 'completed' ? 'completed' : 'active'); + } @endphp @@ -157,14 +174,41 @@ heading="Verification report" :description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification operation.'" > - @if ($run === null) -
- No verification operation has been started yet. + @if ($runState === 'no_run') +
+
+ No verification operation has been started yet. +
+ +
+ Use the workflow action above to start verification for this tenant. +
- @elseif ($status !== 'completed') -
+ @elseif ($runState === 'active') +
Report unavailable while the operation is in progress. Stored status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.
+ +
+ @if ($runUrl) + + {{ \App\Support\OperationRunLinks::openLabel() }} + + @endif + + + Technical details + +
@else
@@ -226,6 +270,27 @@
+
+ @if ($runUrl) + + {{ \App\Support\OperationRunLinks::openLabel() }} + + @endif + + + Technical details + +
+ @if ($showAssist)
@@ -279,13 +344,6 @@ class="space-y-4" > Passed - - Technical details -
@@ -587,85 +645,6 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
@endif
- -
-
-
-
- Identifiers -
-
-
- Operation ID: - {{ (int) ($run['id'] ?? 0) }} -
-
- Flow: - {{ (string) ($run['type'] ?? '') }} -
- @if ($fingerprint) -
- Fingerprint: - {{ $fingerprint }} -
- @endif -
-
- - @if ($previousRunUrl !== null) - - @endif - - @if ($runUrl !== null) - - @endif - - @if ($targetScope !== []) -
-
- Target scope -
-
- @php - $entraTenantId = $targetScope['entra_tenant_id'] ?? null; - $entraTenantName = $targetScope['entra_tenant_name'] ?? null; - - $entraTenantId = is_string($entraTenantId) && $entraTenantId !== '' ? $entraTenantId : null; - $entraTenantName = is_string($entraTenantName) && $entraTenantName !== '' ? $entraTenantName : null; - @endphp - - @if ($entraTenantName !== null) -
- Entra tenant: - {{ $entraTenantName }} -
- @endif - - @if ($entraTenantId !== null) -
- Entra tenant ID: - {{ $entraTenantId }} -
- @endif -
-
- @endif -
-
@endif
diff --git a/resources/views/filament/modals/onboarding-verification-technical-details.blade.php b/resources/views/filament/modals/onboarding-verification-technical-details.blade.php index 751a0329..f2ddcd78 100644 --- a/resources/views/filament/modals/onboarding-verification-technical-details.blade.php +++ b/resources/views/filament/modals/onboarding-verification-technical-details.blade.php @@ -5,6 +5,9 @@ $runUrl = $runUrl ?? null; $runUrl = is_string($runUrl) && $runUrl !== '' ? $runUrl : null; + $previousRunUrl = $previousRunUrl ?? null; + $previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null; + $status = $run['status'] ?? null; $status = is_string($status) ? $status : null; @@ -142,16 +145,47 @@
- @if ($runUrl) -
- - Open operation in Monitoring (advanced) - + @if ($previousRunUrl || $runUrl) +
+
+ Diagnostics links +
+ +
+ @if ($previousRunUrl) +
+ + Open previous operation + + +
+ Compare against the prior verification run without changing the onboarding workflow. +
+
+ @endif + + @if ($runUrl) +
+ + {{ \App\Support\OperationRunLinks::advancedMonitoringLabel() }} + + +
+ {{ \App\Support\OperationRunLinks::advancedMonitoringDescription() }} +
+
+ @endif +
@endif @endif diff --git a/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php b/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php index 48840cf1..c6330ce9 100644 --- a/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php +++ b/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php @@ -2,18 +2,11 @@ /** @var ?\App\Models\Tenant $tenant */ /** @var \Illuminate\Support\Collection $runs */ /** @var string $operationsIndexUrl */ + /** @var string $operationsIndexLabel */ + /** @var string $operationsIndexDescription */ @endphp - - - View all operations - - - @if ($runs->isEmpty())
No operations yet. @@ -69,5 +62,20 @@ class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-pri @endforeach + + @if (! empty($operationsIndexUrl)) +
+
+ {{ $operationsIndexDescription }} +
+ + + {{ $operationsIndexLabel }} + +
+ @endif @endif diff --git a/resources/views/filament/widgets/tenant/tenant-verification-report.blade.php b/resources/views/filament/widgets/tenant/tenant-verification-report.blade.php index 5a6855a5..f5776822 100644 --- a/resources/views/filament/widgets/tenant/tenant-verification-report.blade.php +++ b/resources/views/filament/widgets/tenant/tenant-verification-report.blade.php @@ -20,6 +20,9 @@ $lifecycleNotice = $lifecycleNotice ?? null; $lifecycleNotice = is_string($lifecycleNotice) && trim($lifecycleNotice) !== '' ? trim($lifecycleNotice) : null; + + $rerunHint = $rerunHint ?? null; + $rerunHint = is_string($rerunHint) && trim($rerunHint) !== '' ? trim($rerunHint) : null; @endphp Open operation @endif - - @if ($showStartAction) - @if ($canStart) - - Start verification - - @else -
- - Start verification - - - @if ($startTooltip) -
- {{ $startTooltip }} -
- @endif -
- @endif - @elseif ($lifecycleNotice) -
- {{ $lifecycleNotice }} -
- @endif
+ + @if ($rerunHint || $startTooltip || $lifecycleNotice) +
+ {{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }} +
+ @endif @else @include('filament.components.verification-report-viewer', [ 'run' => $runData, @@ -128,46 +104,19 @@ Open operation @endif - - @if ($showStartAction) - @if ($canStart) - - Start verification - - @else -
- - Start verification - - - @if ($startTooltip) -
- {{ $startTooltip }} -
- @endif -
- @endif - @elseif ($lifecycleNotice) -
- {{ $lifecycleNotice }} -
- @endif
+ + @if ($rerunHint || $startTooltip || $lifecycleNotice) +
+ {{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }} +
+ @endif @endif diff --git a/specs/172-deferred-operator-surfaces-retrofit/checklists/requirements.md b/specs/172-deferred-operator-surfaces-retrofit/checklists/requirements.md index e8579679..4b47252f 100644 --- a/specs/172-deferred-operator-surfaces-retrofit/checklists/requirements.md +++ b/specs/172-deferred-operator-surfaces-retrofit/checklists/requirements.md @@ -32,4 +32,5 @@ ## Feature Readiness ## Notes - Validation pass 1: complete -- This spec intentionally targets only deferred non-table surfaces that already expose operation affordances; unrelated deferred pages remain explicit non-goals unless a later spec enrolls them. \ No newline at end of file +- This spec intentionally targets only deferred non-table surfaces that already expose operation affordances; unrelated deferred pages remain explicit non-goals unless a later spec enrolls them. +- Validation pass 2: aligned route and surface naming with the current implementation. The tenant detail view and onboarding verification surfaces are in scope; the table-based tenant dashboard operations widget remains out of scope for this deferred-surface retrofit. \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml b/specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml new file mode 100644 index 00000000..e0adaa26 --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml @@ -0,0 +1,138 @@ +openapi: 3.1.0 +info: + title: Deferred Embedded Operation Surface Contract + version: 1.0.0 + summary: CTA hierarchy and scope contract for tenant-detail and onboarding surfaces that reference existing OperationRun records. +paths: + /admin/tenants/{record}: + get: + operationId: renderTenantDetailEmbeddedOperationSurfaces + summary: Render tenant-detail embedded widgets that may drill into the canonical operations viewers. + parameters: + - name: record + in: path + required: true + schema: + type: string + responses: + '200': + description: Tenant detail surface rendered successfully. + '403': + description: Authenticated tenant member lacks the required capability within the established tenant scope. + '404': + description: Wrong plane, missing workspace or tenant membership, or inaccessible tenant detail record. + x-surface-rules: + recentOperationsSummary: + canonicalCollectionRoute: /admin/operations + canonicalDetailRoute: /admin/operations/{run} + primaryInspectModel: Row-level Open operation links for displayed records. + collectionAffordance: + allowed: true + prominence: secondary + scopeRequirement: Any remaining collection affordance must make broader admin scope explicit through nearby copy or placement. + forbiddenPatterns: + - A header-level collection CTA with equal emphasis to row-level inspect links. + tenantVerificationWidget: + primaryCtaByState: + noRun: Start verification + activeRun: Open operation + completedRun: Open operation + archivedOrInactive: none + rerunPath: + owner: Tenant detail header action + label: Verify configuration + inlineSecondaryCtasAllowed: [] + x-unchanged-behavior: + - Existing authorization, capability checks, and tenant/workspace isolation remain authoritative. + - Existing OperationRun lifecycle, notification timing, and route helpers remain unchanged. + /admin/onboarding: + get: + operationId: renderOnboardingVerificationOperationSurfaces + summary: Render onboarding verification workflow controls plus embedded report and technical-details surfaces. + responses: + '200': + description: Onboarding verification surface rendered successfully. + '403': + description: Authenticated workspace member lacks the required capability within the established workspace scope. + '404': + description: Wrong plane, missing workspace membership, or inaccessible onboarding context. + x-surface-rules: + workflowControls: + primaryCtaByState: + noRun: Start verification + activeRun: Refresh + completedRun: none + reportSurface: + currentRunInspect: + allowed: true + prominence: primary + labelFamily: + - Open operation + previousRunInspect: + allowed: true + prominence: secondary + placement: diagnostics only + technicalDetails: + advancedMonitoringLink: + allowed: true + prominence: secondary + visibilityRule: Only when the operator can access the destination and the link is explicitly labeled as advanced. + x-unchanged-behavior: + - Existing onboarding workflow semantics, session fields, and verification execution behavior remain unchanged. + - Existing step progression and permission-assist behavior remain unchanged. + /admin/operations: + get: + operationId: listAdminOperations + summary: Canonical admin-plane operations collection used by embedded drill-ins. + responses: + '200': + description: Admin operations collection rendered successfully. + '403': + description: Authenticated member lacks the required capability within an established scope. + '404': + description: Wrong plane, missing scope membership, or inaccessible workspace or tenant context. + x-canonical-role: + role: collection-destination + visibleNoun: Operations + unchangedBehavior: + - Existing route helper remains authoritative. + - This feature only changes how embedded surfaces explain navigation into this collection. + /admin/operations/{run}: + get: + operationId: viewAdminOperation + summary: Canonical admin-plane operation detail used by embedded drill-ins. + parameters: + - name: run + in: path + required: true + schema: + type: integer + responses: + '200': + description: Admin operation detail rendered successfully. + '403': + description: Authenticated member lacks the required capability within an established scope. + '404': + description: Wrong plane, missing scope membership, or inaccessible operation record. + x-canonical-role: + role: detail-destination + visibleNoun: Operation + unchangedBehavior: + - Existing route helper remains authoritative. + - Existing membership and capability checks remain unchanged. + /admin/t/{tenant}: + get: + operationId: tenantDashboardReference + summary: Reference route for the table-based tenant dashboard operations widget. + parameters: + - name: tenant + in: path + required: true + schema: + type: string + responses: + '200': + description: Tenant dashboard rendered successfully. + x-scope-status: + status: out-of-scope + rationale: The table-based recent-operations widget on the tenant dashboard is already declaration-backed and is not part of the deferred embedded-surface retrofit. \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/data-model.md b/specs/172-deferred-operator-surfaces-retrofit/data-model.md new file mode 100644 index 00000000..9f9f2471 --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/data-model.md @@ -0,0 +1,180 @@ +# Data Model: Deferred Operator Surfaces Retrofit + +## Overview + +This feature introduces no new persisted entity, no new table, and no new state family. It reuses existing `OperationRun` truth plus existing tenant-detail and onboarding presentation state to enforce clearer CTA hierarchy and scope signals. + +## Entity: OperationRun + +- **Type**: Existing persisted model +- **Purpose in this feature**: Canonical record whose existing admin-plane collection/detail routes remain the inspect targets for tenant-detail and onboarding embedded surfaces. + +### Relevant Fields + +| Field | Type | Notes | +|-------|------|-------| +| `id` | integer | Existing operation identifier used by embedded links and labels. | +| `type` | string | Continues to determine the operation label and UX guidance. | +| `workspace_id` | integer nullable | Preserves workspace-context routing and entitlement checks. | +| `tenant_id` | integer nullable | Preserves tenant-context entitlement and embedded-surface filtering. | +| `status` | string | Drives whether the current run is still active. | +| `outcome` | string | Drives blocked/failed/succeeded summary presentation. | +| `context` | array/json | Already carries target-scope, verification-report, and next-step metadata used by the affected surfaces. | +| `created_at` | timestamp | Used for recency display on recent-operations surfaces. | +| `started_at` / `completed_at` | timestamp nullable | Used for in-progress vs completed display and technical details. | + +### Relationships + +| Relationship | Target | Purpose | +|--------------|--------|---------| +| `tenant` | `Tenant` | Keeps tenant entitlement and tenant detail context explicit. | +| `workspace` | `Workspace` | Preserves workspace-context authorization for onboarding and admin viewers. | +| `initiator` | `User` / platform initiator context | Remains unchanged; no notification or lifecycle behavior changes in this feature. | + +### Feature-Specific Invariants + +- `/admin/operations` and `/admin/operations/{run}` remain the canonical collection/detail destinations. +- No new `OperationRun` type, state transition, notification timing, or summary-count behavior is introduced. +- Embedded surfaces may change CTA hierarchy and nearby scope copy, but not the underlying destination semantics. + +### State Transitions Used By This Feature + +| Transition | Preconditions | Result | +|------------|---------------|--------| +| Render inspect link for existing record | A covered surface already has a current `OperationRun` reference | No state change; surface exposes a single primary inspect path for that record. | +| Render workflow-start action | Covered surface has no current `OperationRun` reference and the operator can start the workflow | No state change; surface exposes one next-step CTA such as `Start verification`. | + +## Derived Surface State: Tenant Detail Recent Operations Summary + +- **Type**: Existing derived widget state, not persisted +- **Sources**: + - `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` + - `app/Filament/Widgets/Tenant/RecentOperationsSummary.php` + - `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php` + - `app/Support/OperationRunLinks.php` + +### Relevant Fields + +| Field | Type | Purpose | +|-------|------|---------| +| `tenant` | `Tenant` | Provides the current tenant context for filtering and copy. | +| `runs` | collection of `OperationRun` | Existing recent operation records rendered in the embedded summary. | +| `operationsIndexUrl` | string | Existing admin-plane collection destination. | +| `rowOperationUrl` | string derived | Existing admin-plane detail destination per rendered run. | +| `hasRuns` | boolean derived | Distinguishes empty vs populated summary state. | + +### Feature-Specific Invariants + +- Row-level `Open operation` remains the primary inspect affordance for displayed records. +- Any collection drill-in that remains visible is secondary and must make the broader admin scope explicit through placement or nearby helper text. +- The table-based recent-operations widget on `/admin/t/{tenant}` remains out of scope for this model. + +### State Rules + +| State | Preconditions | Primary CTA | Secondary CTA | +|-------|---------------|-------------|---------------| +| Empty summary | `runs` is empty | None or a single next-step/collection affordance if retained | Admin-scope collection link only if clearly secondary | +| Populated summary | `runs` is not empty | Per-row `Open operation` for each visible record | One secondary collection affordance at most | + +## Derived Surface State: Tenant Verification Widget + +- **Type**: Existing derived widget state, not persisted +- **Sources**: + - `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` + - `app/Filament/Widgets/Tenant/TenantVerificationReport.php` + - `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php` + +### Relevant Fields + +| Field | Type | Purpose | +|-------|------|---------| +| `run` | `OperationRun` nullable | Current verification-backed operation, if one exists. | +| `runUrl` | string nullable | Canonical inspect path for the current run. | +| `report` | array nullable | Stored verification report payload displayed read-only. | +| `isInProgress` | boolean | Distinguishes active vs completed run state. | +| `showStartAction` | boolean | Whether the embedded surface may expose a workflow-start CTA in empty-state conditions. | +| `canStart` | boolean | Whether the current actor can start verification. | +| `startTooltip` | string nullable | Existing permission helper text for disabled start states. | +| `lifecycleNotice` | string nullable | Existing archived/inactive-tenant explanation when starting work is not allowed. | + +### Feature-Specific Invariants + +- When no run exists and the tenant can be operated, the widget exposes one primary `Start verification` CTA. +- When a run exists, the widget exposes one primary inspect CTA for that run and does not compete with an inline rerun CTA. +- The existing tenant-detail header action remains the rerun/start path when the page still needs one outside the embedded widget. +- Archived or inoperable tenants may show explanation text, but no new inspect or start path is introduced by this feature. + +### State Matrix + +| State | Preconditions | Primary CTA | Secondary Inline CTA | +|-------|---------------|-------------|----------------------| +| No run / start allowed | `run` is null and `showStartAction && canStart` | `Start verification` | None | +| No run / cannot start | `run` is null and `! canStart` | None | None | +| Active run | `run` exists and `isInProgress` | `Open operation` | None | +| Completed or stale run | `run` exists and `! isInProgress` | `Open operation` | None | +| Archived / inactive tenant | `showStartAction` is false and `lifecycleNotice` is present | None | None | + +## Derived Surface State: Onboarding Verification Surface + +- **Type**: Existing guided-flow report state, not persisted +- **Sources**: + - `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` + - `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php` + - `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` + - `managed_tenant_onboarding_sessions.state[verification_operation_run_id]` + +### Relevant Fields + +| Field | Type | Purpose | +|-------|------|---------| +| `verification_operation_run_id` | integer nullable | Existing onboarding-session pointer to the current verification run. | +| `run` | array nullable | Read-only operation data rendered in the onboarding report. | +| `runUrl` | string nullable | Canonical inspect path for the current run. | +| `previousRunUrl` | string nullable | Secondary link to the previously relevant run, when retained. | +| `report` | array nullable | Stored verification report payload rendered in the onboarding step. | +| `workflowPrimaryAction` | derived string nullable | Existing step-level CTA such as `Start verification` or `Refresh`. | +| `technicalDetailsVisible` | boolean derived | Controls whether advanced monitoring/context affordances are available. | + +### Feature-Specific Invariants + +- The wizard step owns workflow-next-step controls such as `Start verification` and `Refresh`. +- The embedded report/technical-details surfaces may expose one inspect CTA for the current run, but previous-run and monitoring links remain diagnostics-secondary only. +- Any advanced monitoring/admin destination is visible only when the destination is legitimate for the current operator and remains explicitly labeled as advanced. +- No new onboarding state, session field, or workflow branch is introduced. + +### State Matrix + +| State | Preconditions | Workflow CTA | Inspect CTA | Diagnostics CTA | +|-------|---------------|--------------|-------------|-----------------| +| No run | onboarding session has no current verification run | `Start verification` | None | None | +| Active run | current verification run exists and is not completed | `Refresh` | One current-run inspect link if retained on the report surface | Advanced monitoring only in technical details | +| Completed run | current verification run exists and is completed | None or existing step progression controls | One current-run inspect link | Previous-run and monitoring links remain secondary | + +## Governance Artifact: Deferred Surface Exemption And Conformance Coverage + +- **Type**: Existing registry and test coverage, not persisted +- **Sources**: + - `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` + - `tests/Feature/Guards/ActionSurfaceContractTest.php` + - focused widget/onboarding feature tests listed in the plan + +### Relevant Fields + +| Field | Type | Purpose | +|-------|------|---------| +| `className` | string | Exempted page/component class still outside declaration-backed discovery. | +| `reason` | string | Concrete justification for the current exemption. | +| `focusedTests` | derived list | Dedicated conformance coverage that protects the exempted surface behavior. | + +### Feature-Specific Invariants + +- `ManagedTenantOnboardingWizard` may remain baseline-exempt if the reason continues to point to dedicated conformance tests. +- This feature does not introduce a new widget declaration system or a new validator mode. +- Governance for the retrofitted surfaces should become narrower and more explicit, not broader. + +## Persistence Impact + +- **Schema changes**: None +- **Data migration**: None +- **New indexes**: None +- **Retention impact**: None \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/plan.md b/specs/172-deferred-operator-surfaces-retrofit/plan.md new file mode 100644 index 00000000..cd0e2e7f --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/plan.md @@ -0,0 +1,134 @@ +# Implementation Plan: Deferred Operator Surfaces Retrofit + +**Branch**: `172-deferred-operator-surfaces-retrofit` | **Date**: 2026-03-31 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/172-deferred-operator-surfaces-retrofit/spec.md` +**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/172-deferred-operator-surfaces-retrofit/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. + +Retrofit the existing embedded operation-bearing surfaces on the tenant detail page and onboarding verification flow so each state exposes one clear primary action, keeps scope truthful before navigation, and preserves the current canonical `OperationRun` destinations. The implementation stays narrow: reuse the existing Filament widgets, Blade partials, route helpers, and page-level actions; do not add routes, persistence, capabilities, assets, or a new embedded-surface framework. + +## Technical Context + +**Language/Version**: PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 +**Primary Dependencies**: `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` +**Storage**: PostgreSQL with existing `operation_runs`, `managed_tenant_onboarding_sessions`, tenant records, and workspace records; no schema changes +**Testing**: Pest feature, Livewire, and browser-style UI coverage executed through Laravel Sail +**Target Platform**: Laravel web application running in Sail locally and containerized Linux environments for staging and production +**Project Type**: Laravel monolith with admin, tenant, and system Filament panels plus shared Blade partials +**Performance Goals**: Preserve current DB-only render paths for tenant detail widgets and onboarding verification reports; add no remote calls, no new queued work, no new polling cadence, and no broader query fan-out than the current run lookups +**Constraints**: `/admin/operations` and `/admin/operations/{run}` remain the canonical inspect destinations; no new route family, capability, persistence artifact, or `OperationRun` lifecycle change is allowed; tenant detail widgets must not render equal-weight competing CTAs; advanced monitoring links stay secondary and access-aware; no provider-registration, global-search, or asset-pipeline change is in scope +**Scale/Scope**: Two tenant-detail embedded widgets, one onboarding verification report surface plus technical-details modal, one baseline exemption/governance note, and a focused set of Pest/Livewire regression tests + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- `PASS` Inventory-first / snapshots-second: the feature does not change inventory truth, snapshot truth, or backup behavior; it only reorders and relabels derived inspection affordances. +- `PASS` Read/write separation: no new write path or long-running workflow is introduced; existing verification-start actions remain the same and only embedded presentation around existing `OperationRun` records is retrofitted. +- `PASS` Graph contract path: no Microsoft Graph contract or outbound-call path is changed. +- `PASS` Deterministic capabilities: no capability registry, role mapping, or authorization primitive is introduced or modified. +- `PASS` RBAC-UX plane separation: tenant-detail surfaces may continue linking to canonical admin-plane operation viewers, but they must do so without widening access; non-members remain 404 and member-but-missing-capability remains 403 on the existing destinations. +- `PASS` Workspace and tenant isolation: no new tenantless shortcut is introduced; current workspace context, tenant membership, and canonical route guards remain authoritative. +- `PASS` Destructive confirmation standard: no destructive action is added or altered in this slice. +- `PASS` Run observability / Ops-UX lifecycle: the plan reuses existing `OperationRun` records and route helpers only; no status/outcome transition logic, summary-count semantics, notification timing, or run-creation rules are changed. +- `PASS` Proportionality / abstraction / persistence / state (`PROP-001`, `ABSTR-001`, `PERSIST-001`, `STATE-001`, `BLOAT-001`): the feature adds no persistence, abstraction, enum, reason family, or semantic framework and instead narrows drift on existing current-release surfaces. +- `PASS` UI taxonomy and inspect model (`UI-CONST-001`, `UI-SURF-001`, `UI-HARD-001`): the affected surfaces remain embedded widgets and guided-flow report surfaces; the work reduces competing affordances instead of introducing new surface types or inspect models. +- `PASS` Operator surface rules (`OPSURF-001`): default-visible content stays operator-first, while diagnostics and advanced monitoring links remain secondary and explicitly revealed. +- `PASS` UI naming and Filament-native UI (`UI-NAMING-001`, `UI-FIL-001`): the feature reuses existing Filament sections, buttons, widgets, and Blade views, and keeps canonical `Operations` / `Operation` nouns without inventing a local presentation layer. +- `PASS` Testing truth (`TEST-TRUTH-001`): the design will extend focused widget, onboarding, and guard tests rather than introducing a broad string-ban or framework-only conformance layer. +- `PASS` Filament v5 / Livewire v4 guardrails: all touched surfaces already run on Filament v5 and Livewire v4; panel provider registration remains unchanged in `bootstrap/providers.php`; no new globally searchable resource is introduced; no asset strategy changes are needed, so deployment requirements for `filament:assets` are unchanged. + +## Project Structure + +### Documentation (this feature) + +```text +specs/172-deferred-operator-surfaces-retrofit/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ └── embedded-operation-surface-contract.yaml +└── tasks.md +``` + +### Source Code (repository root) + +```text +app/ +├── Filament/ +│ ├── Pages/ +│ │ └── Workspaces/ +│ │ └── ManagedTenantOnboardingWizard.php +│ ├── Resources/ +│ │ └── TenantResource/ +│ │ └── Pages/ +│ │ └── ViewTenant.php +│ └── Widgets/ +│ └── Tenant/ +│ ├── RecentOperationsSummary.php +│ └── TenantVerificationReport.php +├── Support/ +│ ├── OperationRunLinks.php +│ └── Ui/ +│ └── ActionSurface/ +│ └── ActionSurfaceExemptions.php + +resources/ +└── views/ + └── filament/ + ├── forms/ + │ └── components/ + │ └── managed-tenant-onboarding-verification-report.blade.php + ├── modals/ + │ └── onboarding-verification-technical-details.blade.php + └── widgets/ + └── tenant/ + ├── recent-operations-summary.blade.php + └── tenant-verification-report.blade.php + +tests/ +└── Feature/ + ├── Filament/ + │ ├── RecentOperationsSummaryWidgetTest.php + │ └── TenantVerificationReportWidgetTest.php + ├── Guards/ + │ └── ActionSurfaceContractTest.php + └── Onboarding/ + ├── OnboardingVerificationClustersTest.php + ├── OnboardingVerificationTest.php + └── OnboardingVerificationV1_5UxTest.php +``` + +**Structure Decision**: This is a single Laravel application. The implementation stays inside existing tenant-detail widgets, onboarding report views, shared route helpers, and guard tests. No new panel, route family, base directory, or abstraction layer is needed. + +**Focused test inventory (authoritative)**: `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` are the core regression surfaces for this slice. + +## Complexity Tracking + +No constitution waiver is expected. This slice intentionally avoids new persistence, abstractions, and UI frameworks. + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| None | Not applicable | Not applicable | + +## Proportionality Review + +- **Current operator problem**: tenant-detail widgets and onboarding verification surfaces currently expose operation drill-ins with ambiguous scope and competing inline calls to action, which makes it unclear whether the operator should inspect existing execution truth or start new work. +- **Existing structure is insufficient because**: the current behavior is split across Blade branches, widget view-data helpers, and broad deferred-surface exemptions, so one-off wording fixes would not reliably enforce CTA hierarchy or scope-truth across the covered surfaces. +- **Narrowest correct implementation**: retrofit the existing tenant-detail widgets and onboarding verification views in place, reuse the canonical admin operation routes, and rely on the existing tenant-detail header action and onboarding step controls for rerun/start flows instead of adding a new embedded-action framework. +- **Ownership cost created**: low. The feature adds a surface contract artifact plus a focused set of widget/onboarding/governance assertions, but no schema, queue, routing, or abstraction maintenance burden. +- **Alternative intentionally rejected**: creating tenant-scoped operations routes, inventing a widget-specific action-surface declaration framework, or redesigning the entire tenant dashboard/tenant detail experience was rejected because the current-release need is limited to a few embedded operation-bearing surfaces. +- **Release truth**: current-release truth. The covered surfaces already ship today and already expose operation affordances that need clearer hierarchy and scope semantics. + +## Post-Design Constitution Re-check + +- `PASS` `UI-CONST-001` / `UI-SURF-001` / `UI-HARD-001`: the design keeps embedded summaries and guided-flow reports in their existing surface classes and narrows each to one primary inline action model instead of adding a new inspect mechanism. +- `PASS` `OPSURF-001`: tenant-detail widgets answer what happened and where to inspect it next, while onboarding keeps workflow controls (`Start verification`, `Refresh`) separate from diagnostics-only operation links. +- `PASS` `RBAC-UX-001` through `RBAC-UX-005`: existing server-side authorization, 404 vs 403 semantics, and confirmation rules remain unchanged because the design only changes CTA hierarchy and visible scope cues. +- `PASS` `UI-NAMING-001`: canonical `Operations` / `Operation` nouns remain stable across tenant-detail and onboarding inspect affordances without introducing scope-first labels. +- `PASS` `TEST-TRUTH-001`: the design expands focused Pest/Livewire coverage for CTA count, explicit scope cues, and advanced-link visibility rather than codifying a new cross-widget framework. +- `PASS` `BLOAT-001`: no new persistence, abstraction, state family, taxonomy, or presenter layer was added during design. +- `PASS` Filament v5 / Livewire v4 implementation contract: the plan touches existing Filament v5 and Livewire v4 widgets/pages only; panel providers remain registered in `bootstrap/providers.php`; no globally searchable resource changes are introduced; no destructive actions are added or modified; no asset registration changes are needed, so the deployment `filament:assets` step is unaffected. +- `PASS` Testing plan: implementation coverage will target `RecentOperationsSummaryWidgetTest`, `TenantVerificationReportWidgetTest`, `OnboardingVerificationTest`, `OnboardingVerificationClustersTest`, `OnboardingVerificationV1_5UxTest`, and `ActionSurfaceContractTest`. diff --git a/specs/172-deferred-operator-surfaces-retrofit/quickstart.md b/specs/172-deferred-operator-surfaces-retrofit/quickstart.md new file mode 100644 index 00000000..4c7233d4 --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/quickstart.md @@ -0,0 +1,85 @@ +# Quickstart: Deferred Operator Surfaces Retrofit + +## Goal + +Retrofit the tenant-detail and onboarding embedded operation surfaces so they present one clear primary action per state, keep scope truthful before navigation, and leave existing `OperationRun` routes, authorization, and lifecycle behavior unchanged. + +## Prerequisites + +1. Start the local stack: + +```bash +vendor/bin/sail up -d +``` + +2. Work on branch `172-deferred-operator-surfaces-retrofit`. + +## Implementation Steps + +1. Align the tenant-detail embedded surfaces first: + - `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` + - `app/Filament/Widgets/Tenant/RecentOperationsSummary.php` + - `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php` + - `app/Filament/Widgets/Tenant/TenantVerificationReport.php` + - `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php` + - `app/Support/OperationRunLinks.php` only if a secondary collection affordance needs clearer admin-scope copy +2. Make the CTA hierarchy explicit on tenant detail: + - recent-operations summary keeps row-level inspect links primary and any collection link secondary + - verification widget uses `Start verification` only when no run exists and relies on the existing tenant-detail header action for reruns once a run exists +3. Align the onboarding verification surfaces without changing workflow semantics: + - `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` + - `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php` + - `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` + - keep the wizard's existing next-step controls (`Start verification`, `Refresh`) authoritative + - keep current-run inspect links singular and keep previous-run/monitoring links diagnostics-secondary only +4. Narrow the governance story instead of creating a new framework: + - update `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` only if the baseline reason needs to become more specific + - update `tests/Feature/Guards/ActionSurfaceContractTest.php` if the exemption or dedicated-conformance expectation changes +5. Re-run a final pass over the covered views to ensure there is no equal-weight combination of: + - collection CTA plus row/detail CTA on the same embedded summary + - `Start verification` plus `Open operation` inside the same tenant-detail widget state + - `Open operation`, `Open previous operation`, and `Open operation in Monitoring (advanced)` all appearing as peer actions on the onboarding report surface + +## Tests To Update + +1. Tenant-detail embedded surface coverage: + - `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` + - `tests/Feature/Filament/TenantVerificationReportWidgetTest.php` +2. Onboarding verification coverage: + - `tests/Feature/Onboarding/OnboardingVerificationTest.php` + - `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php` + - `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php` +3. Governance coverage: + - `tests/Feature/Guards/ActionSurfaceContractTest.php` + +## Focused Verification + +Run the narrow regression set first: + +```bash +vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php +vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantVerificationReportWidgetTest.php +vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationTest.php +vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationClustersTest.php +vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php +vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php +``` + +If the implementation touches shared operation-link wording or helper text used outside the immediate surfaces, run the smallest additional focused tests that render those helpers. + +## Formatting + +After code changes, run: + +```bash +vendor/bin/sail bin pint --dirty --format agent +``` + +## Manual Review Checklist + +1. The tenant-detail recent-operations summary does not present a header-level collection link with the same visual weight as row-level inspect links. +2. The tenant verification widget shows `Start verification` only when no current run exists and otherwise shows one primary inspect link for the current run. +3. The onboarding flow keeps one workflow-next-step control per state and does not present current-run, previous-run, and advanced-monitoring links as peer primary actions. +4. Any remaining broader-scope operations collection link makes that broader admin scope explicit through context or nearby copy. +5. Any remaining advanced monitoring/admin link is visibly secondary and only appears when the operator can access that destination. +6. No route, capability, persistence artifact, provider registration, or `OperationRun` lifecycle behavior changed. \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/research.md b/specs/172-deferred-operator-surfaces-retrofit/research.md new file mode 100644 index 00000000..d455b389 --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/research.md @@ -0,0 +1,49 @@ +# Research: Deferred Operator Surfaces Retrofit + +## Decision 1: Treat the tenant detail view, not `/admin/t/{tenant}`, as the primary tenant-plane retrofit surface + +- **Decision**: Scope the tenant-plane retrofit to the embedded widgets on `ViewTenant` and keep the table-based recent-operations widget on `/admin/t/{tenant}` out of scope. +- **Rationale**: Repo inspection shows the deferred embedded operation surfaces live on `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, while `app/Filament/Pages/TenantDashboard.php` already uses a declaration-backed table widget with row-click inspection. Mixing both into one slice would blur two different action-surface models. +- **Alternatives considered**: Retrofit the tenant dashboard table widget in the same spec. Rejected because it is already governed as a table surface and would expand the slice beyond the deferred embedded-surface problem. + +## Decision 2: Keep `/admin/operations` and `/admin/operations/{run}` as the canonical inspect destinations + +- **Decision**: Reuse the existing admin-plane operations collection and detail routes for embedded drill-ins rather than creating tenant-scoped operations routes or a parallel tenant viewer. +- **Rationale**: `OperationRunLinks` already centralizes these destinations, and the spec explicitly forbids route or lifecycle changes. The missing behavior is scope-truth before navigation, not a missing destination. +- **Alternatives considered**: Add tenant-scoped operations routes or query-prefiltered viewers. Rejected because that would introduce new routing, new navigation semantics, and more RBAC surface area than this retrofit requires. + +## Decision 3: Use a state-driven CTA hierarchy instead of adding new embedded controls + +- **Decision**: Model each embedded surface around a small CTA matrix: no run means one workflow-start action, an existing run means one primary inspect action, and any broader collection or monitoring links become explicitly secondary. +- **Rationale**: The current drift comes from equal-weight CTAs such as `View all operations` alongside per-row `Open operation` or inline `Start verification` beside an existing `Open operation`. A state matrix solves that with minimal code churn. +- **Alternatives considered**: Keep all current links and only rename them. Rejected because naming alone would not remove the competing-action problem described by Spec 172. + +## Decision 4: Let the owning page or wizard keep rerun controls while embedded surfaces focus on inspection + +- **Decision**: On tenant detail, the existing page-level `Verify configuration` header action remains the rerun/start path when a verification run already exists, while the embedded widget focuses on inspecting the current run. On onboarding, the step-level workflow controls (`Start verification` or `Refresh`) remain the next-step controls, while report/technical-details links stay inspection-oriented. +- **Rationale**: The rerun affordance already exists on the owning surfaces. Reusing it avoids adding a second equal-weight CTA inside the embedded report widgets. +- **Alternatives considered**: Keep rerun/start controls inline inside every embedded surface state. Rejected because it produces the same CTA competition the feature is meant to eliminate. + +## Decision 5: Keep advanced monitoring links in diagnostics-only slots and gate them by existing access + +- **Decision**: Retain any broader monitoring/admin operation link only in low-emphasis technical-details or diagnostics areas, and only when the operator can access that destination. +- **Rationale**: The technical-details modal already has an explicit advanced-monitoring affordance. Keeping it secondary preserves operator clarity without removing legitimate escalation paths for power users. +- **Alternatives considered**: Show advanced monitoring links beside every primary inspect CTA. Rejected because it weakens the single-primary-action rule and increases scope ambiguity. + +## Decision 6: Narrow governance through focused tests and baseline-exemption notes, not through a new widget framework + +- **Decision**: Reuse the existing `ActionSurfaceExemptions` and `ActionSurfaceContractTest` model, tighten the relevant exemption wording if needed, and add focused widget/onboarding coverage instead of introducing widget-specific `actionSurfaceDeclaration()` plumbing. +- **Rationale**: Embedded widgets and report partials do not fit the existing table/page declaration model cleanly, and the codebase already recognizes that `ManagedTenantOnboardingWizard` is covered through dedicated conformance tests. +- **Alternatives considered**: Add a new generic embedded-surface declaration framework. Rejected because one concrete retrofit does not justify a new abstraction under `ABSTR-001` and `BLOAT-001`. + +## Decision 7: Verify behavior with focused Pest and Livewire tests instead of broad string guards + +- **Decision**: Extend the existing widget and onboarding feature tests to assert CTA count, scope cues, and advanced-link visibility directly on rendered surfaces. +- **Rationale**: The business truth here is UI hierarchy and scope behavior, not just string presence. Focused rendered-surface tests are more precise than grep-style bans and align with `TEST-TRUTH-001`. +- **Alternatives considered**: Add a repo-wide architecture or grep-style rule forbidding specific link combinations. Rejected because valid out-of-scope surfaces and diagnostics would require an exception list that encodes the same complexity this slice is avoiding. + +## Decision 8: Make scope explicit through placement and nearby copy rather than scope-first labels + +- **Decision**: Any remaining collection drill-in from a tenant-detail surface should communicate broader admin scope through placement or helper text, not by inventing scope-first primary labels. +- **Rationale**: `UI-NAMING-001` forbids making scope the primary verb-object label. The operator still needs to understand when a link leaves the tenant-local shell, but that should come from context and secondary explanation. +- **Alternatives considered**: Rename the main CTA to `Open admin operations`. Rejected because it over-rotates toward implementation language and conflicts with the repo's operator-facing naming rules. \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/spec.md b/specs/172-deferred-operator-surfaces-retrofit/spec.md index ecf54ffe..4709d738 100644 --- a/specs/172-deferred-operator-surfaces-retrofit/spec.md +++ b/specs/172-deferred-operator-surfaces-retrofit/spec.md @@ -9,14 +9,14 @@ ## Spec Scope Fields *(mandatory)* - **Scope**: workspace + tenant + canonical-view - **Primary Routes**: - - `/admin/t/{tenant}` + - `/admin/tenants/{record}` - `/admin/operations` - `/admin/operations/{run}` - - managed-tenant onboarding flow routes that expose verification operation reports + - `/admin/onboarding` and related onboarding verification technical-details surfaces - **Data Ownership**: - No new platform-owned, workspace-owned, or tenant-owned records are introduced - Existing `OperationRun` records remain the only source of truth for operation status, deep-link destinations, and verification history - - Tenant dashboard widgets, onboarding report components, and related embedded operation affordances remain derived presentation layers only + - Tenant detail widgets, onboarding report components, and related embedded operation affordances remain derived presentation layers only - **RBAC**: - No new capability family is introduced - Existing tenant, workspace, and admin access rules remain authoritative for every destination that a retrofitted surface may open @@ -26,7 +26,7 @@ ## UI/UX Surface Classification *(mandatory when operator-facing surfaces are ch | Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type | |---|---|---|---|---|---|---|---|---|---|---|---| -| Tenant dashboard operation cards and widgets | Embedded status summary / drill-in surface | Explicit CTA to a tenant-scoped operations destination | forbidden | card footer or secondary widget action only | none | tenant-scoped operations destination in admin panel | panel-appropriate operation detail when singular | current tenant remains explicit before navigation | Operations / Operation | active or recent operation truth, tenant context, next destination | retrofit existing deferred surface | +| Tenant detail recent-operations summary | Embedded status summary / drill-in surface | Explicit CTA to the canonical operation destination from the tenant detail page | forbidden | widget header or footer only | none | panel-appropriate operations collection in admin panel | panel-appropriate operation detail when singular | tenant detail context remains explicit before navigation | Operations / Operation | active or recent operation truth, tenant context, next destination | retrofit existing deferred surface | | Tenant verification report widget | Embedded operator detail panel | One primary inspect CTA to the existing operation when present | forbidden | advanced admin/monitoring link only if justified and clearly secondary | none | panel-appropriate operations collection when needed | panel-appropriate operation detail route | tenant context and current verification state explicit | Operations / Operation | verification state, operation identity, recency | retrofit existing deferred surface | | Managed-tenant onboarding verification report and technical-details surfaces | Guided workflow sub-surface | One primary inspect CTA to the existing operation when present, otherwise one workflow-next-step CTA | forbidden | low-emphasis advanced links only when justified | none | panel-appropriate operations collection when needed | panel-appropriate operation detail route | workspace, tenant, and verification context explicit | Operations / Operation | verification status, operation identity, stale-state explanation | retrofit existing deferred surface | @@ -34,7 +34,7 @@ ## Operator Surface Contract *(mandatory when operator-facing surfaces are chang | Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions | |---|---|---|---|---|---|---|---|---|---| -| Tenant dashboard operation cards and widgets | Tenant operator | Embedded status summary / drill-in surface | What is happening in this tenant, and where do I inspect it? | tenant-scoped count or recent activity, explicit destination scope, one clear CTA | raw operation payloads and extended traces stay on destination surfaces | recency, active-state, failure/stuck summary | none | tenant-scoped operations drill-in | none | +| Tenant detail recent-operations summary | Tenant operator | Embedded status summary / drill-in surface | What is happening in this tenant, and where do I inspect it? | tenant-local recent activity, explicit destination scope, one clear CTA | raw operation payloads and extended traces stay on destination surfaces | recency, active-state, failure/stuck summary | none | operations drill-in from tenant detail context | none | | Tenant verification report widget | Tenant operator | Embedded operator detail panel | What verification operation ran for this tenant, and how do I inspect it? | verification result, one primary operation link, operation identity, timestamp | raw provider diagnostics stay behind explicit reveal or destination detail | verification outcome, recency, stale-state | none | `Open operation` or current-step CTA | none | | Managed-tenant onboarding verification report and technical details | Workspace operator running onboarding | Guided workflow sub-surface | What verification operation supports this onboarding step, and what should I do next? | workflow state, operation identity when present, one primary CTA, scope cue | low-level payloads, hashes, and verbose traces stay in diagnostics sections or canonical detail | workflow status, verification outcome, stale-state | none | `Open operation` or `Start verification` | none | @@ -45,27 +45,27 @@ ## Proportionality Review *(mandatory when structural complexity is introduced)* - **New abstraction?**: No - **New enum/state/reason family?**: No - **New cross-domain UI framework/taxonomy?**: No -- **Current operator problem**: Dashboard widgets, tenant verification widgets, and onboarding verification components still behave like deferred or exempt surfaces even though they expose meaningful operation drill-ins, which leaves CTA count, scope cues, and deep-link behavior underdefined compared with the now-aligned table and detail surfaces. +- **Current operator problem**: Tenant detail widgets, tenant verification widgets, and onboarding verification components still behave like deferred or exempt surfaces even though they expose meaningful operation drill-ins, which leaves CTA count, scope cues, and deep-link behavior underdefined compared with the now-aligned table and detail surfaces. - **Existing structure is insufficient because**: Spec 169 intentionally left these embedded surfaces out of the table-centric action-surface enforcement path, so the repo still permits tenant-context leaks, competing links, and scope-ambiguous operation affordances on high-traffic summary surfaces. - **Narrowest correct implementation**: Retrofit only the deferred non-table surfaces that already expose operation affordances, give them explicit operator contracts and representative coverage, and keep unrelated deferred pages out of scope. -- **Ownership cost**: Existing dashboard and onboarding tests will need to assert CTA count, destination scope, and advanced-link visibility, and the exemption baseline or equivalent governance notes must be narrowed for the retrofitted surfaces. +- **Ownership cost**: Existing tenant-detail and onboarding tests will need to assert CTA count, destination scope, and advanced-link visibility, and the exemption baseline or equivalent governance notes must be narrowed for the retrofitted surfaces. - **Alternative intentionally rejected**: Broadly enrolling every deferred dashboard, chooser, landing page, or page-class route into the main action-surface validator was rejected because this slice only needs to retrofit operation-bearing embedded surfaces, not redesign every deferred surface family. -- **Release truth**: Current-release truth. The tenant dashboard and onboarding verification flows already expose operation links today, and current audits show scope and affordance drift on those surfaces. +- **Release truth**: Current-release truth. The tenant detail and onboarding verification flows already expose operation links today, and current audits show scope and affordance drift on those surfaces. ## User Scenarios & Testing *(mandatory)* -### User Story 1 - Tenant Dashboard Drill-Ins Preserve Tenant Context (Priority: P1) +### User Story 1 - Tenant Detail Drill-Ins Preserve Tenant Context (Priority: P1) -As a tenant operator, I want dashboard operation cards and widgets to send me to a destination that clearly preserves my current tenant context, so that I am not silently dropped into a broader workspace-wide operations surface. +As a tenant operator, I want tenant-detail operation summaries and widgets to send me to a destination that clearly preserves my current tenant context, so that I am not silently dropped into a broader workspace-wide operations surface. -**Why this priority**: Tenant dashboard drill-ins are a frequent entry point and currently carry the clearest cross-scope surprise risk. +**Why this priority**: Tenant-detail drill-ins are a frequent entry point and currently carry the clearest cross-scope surprise risk. -**Independent Test**: Can be fully tested by rendering the relevant tenant dashboard operation affordances and asserting that their visible destination semantics remain tenant-scoped and do not silently link to an unfiltered workspace-wide operations surface. +**Independent Test**: Can be fully tested by rendering the relevant tenant-detail operation affordances and asserting that their visible destination semantics remain tenant-scoped and do not silently link to an unfiltered workspace-wide operations surface. **Acceptance Scenarios**: -1. **Given** a tenant operator on the tenant dashboard, **When** the operator opens a dashboard operation drill-in, **Then** the destination preserves tenant context or makes the broader scope explicit before navigation. -2. **Given** a tenant dashboard widget summarizes recent or active operations, **When** it renders, **Then** it exposes at most one primary operations drill-in rather than multiple competing operation links. +1. **Given** a tenant operator on the tenant detail page, **When** the operator opens an embedded operation drill-in, **Then** the destination preserves tenant context or makes the broader scope explicit before navigation. +2. **Given** a tenant detail widget summarizes recent or active operations, **When** it renders, **Then** it exposes at most one primary operations drill-in rather than multiple competing operation links. --- @@ -91,11 +91,11 @@ ### User Story 3 - Retrofitted Deferred Surfaces Gain Explicit Governance (Prior **Why this priority**: Without explicit governance, the retrofitted surfaces will remain drift-prone even after a one-time UX fix. -**Independent Test**: Can be fully tested by proving that representative tenant dashboard and onboarding verification surfaces are covered by dedicated tests or governance checks, while unrelated deferred surfaces remain explicit non-goals. +**Independent Test**: Can be fully tested by proving that representative tenant-detail and onboarding verification surfaces are covered by dedicated tests or governance checks, while unrelated deferred surfaces remain explicit non-goals. **Acceptance Scenarios**: -1. **Given** the retrofit is complete, **When** representative tests or governance checks run, **Then** tenant dashboard and onboarding verification operation affordances are covered explicitly rather than relying on a blanket deferred exemption. +1. **Given** the retrofit is complete, **When** representative tests or governance checks run, **Then** tenant-detail and onboarding verification operation affordances are covered explicitly rather than relying on a blanket deferred exemption. 2. **Given** unrelated deferred surfaces such as chooser pages or landing pages remain untouched, **When** the retrofit ships, **Then** they remain explicit non-goals rather than being swept in accidentally. ### Edge Cases @@ -103,6 +103,7 @@ ### Edge Cases - A tenant verification surface may need to show no current operation, an in-progress operation, or a stale completed operation; each state still needs one primary next-step affordance. - Some operators may have access to the tenant-local surface but not to an advanced admin monitoring destination; advanced links must respect destination access. - A retrofitted surface may expose a collection drill-in or a single-operation drill-in depending on context, but the scope must remain explicit either way. +- The table-based recent-operations widget on `/admin/t/{tenant}` already has declaration-backed row-click inspection and is not itself part of this deferred embedded-surface retrofit. - Unrelated deferred surfaces such as `ChooseTenant`, `ChooseWorkspace`, `ManagedTenantsLanding`, and the Monitoring Alerts page-class route remain out of scope unless they later gain operation affordances that justify a dedicated spec. ## Requirements *(mandatory)* @@ -113,7 +114,7 @@ ## Requirements *(mandatory)* **Constitution alignment (UI-CONST-001 / UI-SURF-001 / UI-HARD-001 / UI-REVIEW-001):** Even though these are embedded or guided-flow surfaces rather than table pages, they must still expose one clear primary inspect or next-step model, keep scope truthful, and avoid competing actions. -**Constitution alignment (OPSURF-001):** Dashboard and onboarding verification surfaces must be operator-first summary surfaces: the default-visible content should answer what happened, what scope it affected, and where the operator should go next without exposing low-level diagnostics by default. +**Constitution alignment (OPSURF-001):** Tenant-detail and onboarding verification surfaces must be operator-first summary surfaces: the default-visible content should answer what happened, what scope it affected, and where the operator should go next without exposing low-level diagnostics by default. **Constitution alignment (UI-FIL-001):** Existing Filament pages, widgets, and embedded components remain the implementation shape. No local mini-framework for embedded operation actions is introduced. @@ -123,37 +124,37 @@ ## Requirements *(mandatory)* ### Functional Requirements -- **FR-172-001**: Tenant dashboard operation-bearing widgets or cards MUST expose navigation that preserves tenant context or makes any broader scope explicit before the operator leaves the tenant dashboard. +- **FR-172-001**: Tenant-detail operation-bearing widgets or cards MUST expose navigation that preserves tenant context or makes any broader scope explicit before the operator leaves the tenant detail surface. - **FR-172-002**: Retrofitted embedded surfaces that reference one existing operation record MUST expose exactly one primary inspect affordance for that record. - **FR-172-003**: Retrofitted embedded surfaces in a no-history or no-operation state MUST expose exactly one primary next-step CTA on the owning surface and MUST NOT render competing inline operation links. - **FR-172-004**: Any retained advanced admin or monitoring destination link MUST be clearly secondary, explicitly labeled for its scope or audience, and visible only when the operator can access that destination. - **FR-172-005**: Retrofitted tenant or workspace surfaces MUST keep tenant, workspace, or admin scope explicit in their visible copy or destination semantics before navigation occurs. -- **FR-172-006**: Governance artifacts, exemption handling, or dedicated tests MUST stop treating the retrofitted operation-bearing parts of `TenantDashboard` and `ManagedTenantOnboardingWizard` as fully out of scope. +- **FR-172-006**: Governance artifacts, exemption handling, or dedicated tests MUST stop treating the retrofitted operation-bearing parts of the tenant detail view and `ManagedTenantOnboardingWizard` as fully out of scope. - **FR-172-007**: Unrelated deferred surfaces that do not expose operation affordances today, including `ChooseTenant`, `ChooseWorkspace`, `ManagedTenantsLanding`, and the Monitoring Alerts page-class route, MUST remain explicit non-goals for this slice. - **FR-172-008**: Existing operation destinations, authorization rules, and lifecycle semantics MUST remain unchanged. -- **FR-172-009**: Representative automated coverage MUST verify CTA count, scope-truthful navigation, and advanced-link visibility on the tenant dashboard and onboarding verification surfaces affected by this slice. -- **FR-172-010**: This feature MUST NOT introduce a new dashboard page, a new onboarding flow, or a new operations capability. +- **FR-172-009**: Representative automated coverage MUST verify CTA count, scope-truthful navigation, and advanced-link visibility on the tenant-detail and onboarding verification surfaces affected by this slice. +- **FR-172-010**: This feature MUST NOT introduce a new tenant detail page, a new onboarding flow, or a new operations capability. ## UI Action Matrix *(mandatory when Filament is changed)* | Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions | |---|---|---|---|---|---|---|---|---|---|---| -| Tenant dashboard operation cards/widgets | `app/Filament/Pages/TenantDashboard.php` and its operation-bearing widgets | existing dashboard/header actions remain | n/a | one explicit operations drill-in per card/widget maximum | n/a | existing surface-specific next-step CTA remains singular | n/a | n/a | no new audit behavior | Retrofit current deferred widget/card surfaces with scope-truthful operations drill-ins | -| Tenant verification report widget | tenant verification widget and embedded report view | existing widget actions remain | n/a | one primary `Open operation` link when a record exists | n/a | owning surface keeps one workflow CTA when no operation exists | n/a | n/a | no new audit behavior | Advanced admin/monitoring link may remain only as secondary | +| Tenant detail recent-operations summary | `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` and `app/Filament/Widgets/Tenant/RecentOperationsSummary.php` | existing tenant detail header actions remain | n/a | one explicit operations drill-in per embedded summary state maximum | n/a | existing surface-specific next-step CTA remains singular | n/a | n/a | no new audit behavior | Retrofit current deferred embedded summary with scope-truthful operations drill-ins | +| Tenant verification report widget | `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` and `app/Filament/Widgets/Tenant/TenantVerificationReport.php` | existing tenant detail header actions remain | n/a | one primary `Open operation` link when a record exists | n/a | owning surface keeps one workflow CTA when no operation exists | n/a | n/a | no new audit behavior | Advanced admin/monitoring link may remain only as secondary | | Managed-tenant onboarding verification report and technical details | `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` and related onboarding report/modal views | existing workflow actions remain | n/a | one primary `Open operation` link when a record exists | n/a | `Start verification` or equivalent next-step CTA remains singular when no operation exists | n/a | n/a | no new audit behavior | Guided-flow retrofit; no new page or route family | ### Key Entities *(include if feature involves data)* -- **Deferred operation-bearing embedded surface**: Existing dashboard card, widget, report block, or modal that is not a table page but still exposes operation truth or navigation. +- **Deferred operation-bearing embedded surface**: Existing tenant-detail widget, report block, or modal that is not a table page but still exposes operation truth or navigation. - **Primary inspect affordance**: The one visible link or CTA that opens the canonical operation destination for the current embedded context. - **Advanced destination link**: A clearly secondary operation-related link that exposes a broader monitoring destination only for operators who can access it. ## Success Criteria *(mandatory)* -- **SC-172-001**: Representative automated coverage verifies that tenant dashboard operation drill-ins preserve tenant context or make any broader scope explicit before navigation. +- **SC-172-001**: Representative automated coverage verifies that tenant-detail operation drill-ins preserve tenant context or make any broader scope explicit before navigation. - **SC-172-002**: Representative automated coverage verifies that covered verification and onboarding surfaces expose exactly one primary CTA per state. - **SC-172-003**: Representative automated coverage verifies that any retained advanced monitoring/admin link is secondary and access-aware. -- **SC-172-004**: The feature ships without adding a new dashboard page, onboarding flow, operations capability, or persistence artifact. +- **SC-172-004**: The feature ships without adding a new tenant detail page, onboarding flow, operations capability, or persistence artifact. ## Assumptions @@ -163,7 +164,7 @@ ## Assumptions ## Non-Goals -- Building a new workspace home or redesigning the tenant dashboard as a whole +- Building a new workspace home or redesigning the tenant detail experience as a whole - Reworking onboarding flow mechanics or verification execution semantics - Enrolling chooser pages, `ManagedTenantsLanding`, or the Monitoring Alerts page-class route into this retrofit when they do not currently expose operation affordances that need the same treatment - Introducing a broad action-surface framework for all widgets and embedded components beyond the explicit retrofits in this slice @@ -173,8 +174,8 @@ ## Dependencies - Spec 169 deferred-surface exemption baseline - Spec 170 system operations surface alignment - Spec 171 operations naming consolidation -- Existing tenant dashboard widgets, verification report widgets, onboarding verification report components, and canonical admin operation destinations +- Existing tenant detail widgets, verification report widgets, onboarding verification report components, and canonical admin operation destinations ## Definition of Done -Spec 172 is complete when the deferred non-table surfaces that already expose operations, especially tenant dashboard operation drill-ins and onboarding/verification report surfaces, provide one clear primary CTA per state, preserve or explicitly announce scope before navigation, keep any advanced monitoring/admin links secondary and access-aware, and are protected by explicit governance or representative tests instead of relying on a blanket deferred exemption. \ No newline at end of file +Spec 172 is complete when the deferred non-table surfaces that already expose operations, especially tenant-detail operation drill-ins and onboarding/verification report surfaces, provide one clear primary CTA per state, preserve or explicitly announce scope before navigation, keep any advanced monitoring/admin links secondary and access-aware, and are protected by explicit governance or representative tests instead of relying on a blanket deferred exemption. \ No newline at end of file diff --git a/specs/172-deferred-operator-surfaces-retrofit/tasks.md b/specs/172-deferred-operator-surfaces-retrofit/tasks.md new file mode 100644 index 00000000..510cb1d8 --- /dev/null +++ b/specs/172-deferred-operator-surfaces-retrofit/tasks.md @@ -0,0 +1,190 @@ +# Tasks: Deferred Operator Surfaces Retrofit + +**Input**: Design documents from `specs/172-deferred-operator-surfaces-retrofit/` +**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/embedded-operation-surface-contract.yaml`, `quickstart.md` + +**Tests**: Required. Update the focused Pest coverage listed in `specs/172-deferred-operator-surfaces-retrofit/quickstart.md`. +**Operations**: Reuse the existing `OperationRun` lifecycle and canonical `/admin/operations` routes; this feature must not add new run semantics. +**RBAC**: Authorization planes and 404/403 behavior stay unchanged; covered surfaces still need access-aware advanced links. + +## Phase 1: Setup (Shared Context) + +**Purpose**: Confirm the retrofit boundary, affected surfaces, and focused verification targets before implementation starts. + +- [X] T001 Review the implementation boundary in `specs/172-deferred-operator-surfaces-retrofit/spec.md`, `specs/172-deferred-operator-surfaces-retrofit/plan.md`, `specs/172-deferred-operator-surfaces-retrofit/research.md`, and `specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml` +- [X] T002 Baseline the current tenant-detail and onboarding behavior in `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, and `tests/Feature/Onboarding/OnboardingVerificationTest.php` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Put the shared route and CTA handoff in place before changing individual surfaces. + +**⚠️ CRITICAL**: No user story work should start until this phase is complete. + +- [X] T003 Update shared destination metadata in `app/Support/OperationRunLinks.php` so retrofitted surfaces can keep canonical collection/detail routes while making any broader admin scope explicit +- [X] T004 Establish page-level workflow-action ownership in `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` and `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` so embedded surfaces consume inspect-oriented inputs instead of deciding workflow CTAs themselves + +**Checkpoint**: Shared helpers and page-level state handoff are ready, so story work can proceed safely. + +--- + +## Phase 3: User Story 1 - Tenant Detail Drill-Ins Preserve Tenant Context (Priority: P1) 🎯 MVP + +**Goal**: Make the tenant-detail recent-operations and verification widgets expose one clear primary CTA per state without silently broadening scope. + +**Independent Test**: Render the tenant-detail widgets and verify that row-level or current-run inspection stays primary, any collection drill-in is clearly secondary, and the visible scope remains truthful before navigation. + +### Tests for User Story 1 + +- [X] T005 [P] [US1] Update `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` to assert row-level `Open operation` links stay primary and any collection drill-in is secondary and scope-explicit +- [X] T006 [P] [US1] Update `tests/Feature/Filament/TenantVerificationReportWidgetTest.php` to assert `Start verification` and `Open operation` never appear as competing peer CTAs on the widget + +### Implementation for User Story 1 + +- [X] T007 [US1] Refine state assembly in `app/Filament/Widgets/Tenant/RecentOperationsSummary.php` and `app/Filament/Widgets/Tenant/TenantVerificationReport.php` so each tenant-detail surface emits one primary CTA contract per state +- [X] T008 [P] [US1] Update `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php` to demote any collection affordance and make broader admin scope explicit before navigation +- [X] T009 [P] [US1] Update `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php` to render one primary CTA per state and rely on the tenant-detail header action for reruns + +**Checkpoint**: Tenant-detail embedded operation surfaces are independently testable and preserve tenant context truth. + +--- + +## Phase 4: User Story 2 - Onboarding And Verification Surfaces Expose One Clear Operation Path (Priority: P1) + +**Goal**: Keep onboarding verification workflow controls authoritative while report and technical-details surfaces expose one primary inspect path plus diagnostics-secondary links only. + +**Independent Test**: Render onboarding verification surfaces in no-run, active-run, and completed-run states and verify that each state exposes exactly one primary CTA while previous-run or advanced monitoring links remain secondary and access-aware. + +### Tests for User Story 2 + +- [X] T010 [P] [US2] Update `tests/Feature/Onboarding/OnboardingVerificationTest.php` to assert one primary verification CTA per onboarding state +- [X] T011 [P] [US2] Update `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php` and `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php` to assert previous-run and advanced monitoring links stay diagnostics-secondary and access-aware + +### Implementation for User Story 2 + +- [X] T012 [US2] Refine onboarding verification report payload assembly in `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` so current-run, previous-run, and advanced-link data reach the embedded surfaces without introducing inline workflow CTAs +- [X] T013 [P] [US2] Update `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php` to show exactly one primary current-run inspect CTA when a run exists and otherwise render explanatory empty-state content without inline workflow CTAs +- [X] T014 [P] [US2] Update `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` to keep previous-run and advanced monitoring links secondary, explicitly labeled, and diagnostics-only + +**Checkpoint**: Onboarding verification surfaces are independently testable and expose a single clear operator path per state. + +--- + +## Phase 5: User Story 3 - Retrofitted Deferred Surfaces Gain Explicit Governance (Priority: P2) + +**Goal**: Replace blanket deferred-surface treatment for the retrofitted operation-bearing surfaces with explicit governance and representative automated coverage. + +**Independent Test**: Run the guard suite and confirm that tenant-detail and onboarding verification surfaces are covered explicitly, while unrelated deferred surfaces remain intentional non-goals. + +### Tests for User Story 3 + +- [X] T015 [P] [US3] Update `tests/Feature/Guards/ActionSurfaceContractTest.php` to assert tenant-detail and onboarding verification surfaces are covered explicitly rather than by blanket deferred exemptions + +### Implementation for User Story 3 + +- [X] T016 [US3] Narrow retrofit governance in `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` so only true non-goal deferred surfaces remain exempt and dedicated coverage is documented for `ManagedTenantOnboardingWizard` + +**Checkpoint**: Governance now protects the retrofitted surfaces without sweeping unrelated deferred pages into scope. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Run the focused regression and cleanup steps needed to ship the retrofit safely. + +- [X] T017 Run the focused regression suite in `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` +- [X] T018 Run formatting for `app/Support/OperationRunLinks.php`, `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`, `app/Filament/Widgets/Tenant/TenantVerificationReport.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, and `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` +- [X] T019 Execute the manual validation checklist in `specs/172-deferred-operator-surfaces-retrofit/quickstart.md` against `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`, `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`, `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`, and `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` +- [X] T020 Verify render-path constraints in `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`, `app/Filament/Widgets/Tenant/TenantVerificationReport.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, and `app/Support/OperationRunLinks.php` so the retrofit stays DB-only at render time, adds no remote calls or queued work, does not alter polling cadence, and does not broaden query fan-out + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1: Setup** has no dependencies and starts immediately. +- **Phase 2: Foundational** depends on Phase 1 and blocks all story work. +- **Phase 3: User Story 1** depends on Phase 2. +- **Phase 4: User Story 2** depends on Phase 2. +- **Phase 5: User Story 3** depends on the relevant US1 and US2 implementation and test coverage being in place. +- **Phase 6: Polish** depends on the desired story phases being complete. + +### User Story Dependencies + +- **US1** can start as soon as T003-T004 are complete. +- **US2** can start as soon as T003-T004 are complete. +- **US3** should start after US1 and US2 have landed their representative surface behavior, because the governance guard needs the final retrofit boundary. + +### Parallel Opportunities + +- **US1**: T005 and T006 can run in parallel; after T007, T008 and T009 can run in parallel. +- **US2**: T010 and T011 can run in parallel; after T012, T013 and T014 can run in parallel. +- **US3**: Parallelism is intentionally limited; create the failing guard in T015 first, then narrow the exemption set in T016. + +--- + +## Parallel Example: User Story 1 + +```bash +# Run the tenant-detail widget tests together: +Task: "T005 [US1] Update tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php" +Task: "T006 [US1] Update tests/Feature/Filament/TenantVerificationReportWidgetTest.php" + +# After widget state assembly is updated, align both Blade views together: +Task: "T008 [US1] Update resources/views/filament/widgets/tenant/recent-operations-summary.blade.php" +Task: "T009 [US1] Update resources/views/filament/widgets/tenant/tenant-verification-report.blade.php" +``` + +## Parallel Example: User Story 2 + +```bash +# Update onboarding coverage together: +Task: "T010 [US2] Update tests/Feature/Onboarding/OnboardingVerificationTest.php" +Task: "T011 [US2] Update tests/Feature/Onboarding/OnboardingVerificationClustersTest.php and tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php" + +# After the wizard state contract is ready, update both onboarding views together: +Task: "T013 [US2] Update resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php" +Task: "T014 [US2] Update resources/views/filament/modals/onboarding-verification-technical-details.blade.php" +``` + +## Parallel Example: User Story 3 + +```bash +# No safe implementation pair is recommended here; land the failing guard first, then narrow the exemption baseline: +Task: "T015 [US3] Update tests/Feature/Guards/ActionSurfaceContractTest.php" +Task: "T016 [US3] Update app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete T001-T004. +2. Complete T005-T009. +3. Validate tenant-detail behavior with the focused US1 tests before moving on. + +### Incremental Delivery + +1. Complete Setup and Foundational work to lock the shared route and CTA contract. +2. Deliver US1 and validate tenant-detail scope truth. +3. Deliver US2 and validate onboarding CTA hierarchy. +4. Deliver US3 to replace blanket exemptions with explicit governance. +5. Finish with T017-T020. + +### Parallel Team Strategy + +1. One developer completes T001-T004. +2. After Phase 2, one developer can take US1 while another takes US2. +3. Once both surfaces are stable, finish US3 governance and the shared regression pass. + +--- + +## Notes + +- `[P]` tasks touch different files and can run in parallel. +- User story labels map directly to the priorities and acceptance criteria in `specs/172-deferred-operator-surfaces-retrofit/spec.md`. +- Keep the implementation narrow: no new routes, persistence, capabilities, assets, or `OperationRun` lifecycle changes. diff --git a/tests/Browser/OnboardingDraftVerificationResumeTest.php b/tests/Browser/OnboardingDraftVerificationResumeTest.php index ce313272..4eb2c024 100644 --- a/tests/Browser/OnboardingDraftVerificationResumeTest.php +++ b/tests/Browser/OnboardingDraftVerificationResumeTest.php @@ -105,10 +105,10 @@ ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertSee('Verify access') ->assertSee('Status: Needs attention') - ->assertSee('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.') + ->assertSee('The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.') ->assertSee('Start verification') ->refresh() - ->waitForText('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.') + ->waitForText('The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.') ->assertNoJavaScriptErrors() ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertSee('Status: Needs attention') diff --git a/tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php b/tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php new file mode 100644 index 00000000..345462ba --- /dev/null +++ b/tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php @@ -0,0 +1,318 @@ +browser()->timeout(15_000); + +it('smokes tenant detail with existing operation surfaces and canonical operations routes', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $report = VerificationReportWriter::build('provider.connection.check', [ + [ + 'key' => 'provider.connection.check', + 'title' => 'Provider connection preflight', + 'status' => 'fail', + 'severity' => 'critical', + 'blocking' => true, + 'reason_code' => 'provider_connection_missing', + 'message' => 'No provider connection configured.', + 'evidence' => [], + 'next_steps' => [], + ], + ]); + + $run = OperationRun::factory()->create([ + 'workspace_id' => (int) $tenant->workspace_id, + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Blocked->value, + 'context' => [ + 'target_scope' => [ + 'entra_tenant_id' => (string) $tenant->tenant_id, + ], + 'verification_report' => $report, + ], + ]); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + ]); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + + $page = visit(TenantResource::getUrl('view', ['record' => $tenant->getRouteKey()], panel: 'admin')); + + $page + ->assertNoJavaScriptErrors() + ->assertSee((string) $tenant->name) + ->assertSee('Recent operations') + ->assertSee('Verification report') + ->assertSee(OperationRunLinks::openCollectionLabel()) + ->assertSee(OperationRunLinks::collectionScopeDescription()) + ->assertSee(OperationRunLinks::openLabel()) + ->assertSee(ViewTenant::verificationHeaderActionLabel()) + ->assertDontSee('Start verification') + ->click(OperationRunLinks::openCollectionLabel()) + ->assertNoJavaScriptErrors() + ->assertRoute('admin.operations.index'); + + visit(OperationRunLinks::tenantlessView($run)) + ->assertNoJavaScriptErrors() + ->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()]) + ->assertSee(OperationRunLinks::identifier((int) $run->getKey())); +}); + +it('smokes tenant detail empty verification state without an inline inspect action', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + ]); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + + visit(TenantResource::getUrl('view', ['record' => $tenant->getRouteKey()], panel: 'admin')) + ->assertNoJavaScriptErrors() + ->assertSee('Verification report') + ->assertSee('No verification operation has been started yet.') + ->assertSee('Start verification') + ->assertDontSee(OperationRunLinks::openLabel()); +}); + +it('smokes onboarding verify step without a verification run', function (): void { + $workspace = Workspace::factory()->create(); + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'status' => Tenant::STATUS_ONBOARDING, + ]); + $user = User::factory()->create(['name' => 'Spec172 Browser Owner']); + + createUserWithTenant( + tenant: $tenant, + user: $user, + role: 'owner', + workspaceRole: 'owner', + ensureDefaultMicrosoftProviderConnection: false, + ); + + $connection = ProviderConnection::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'provider' => 'microsoft', + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'is_default' => true, + 'status' => 'connected', + ]); + + TenantOnboardingSession::query()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'current_step' => 'verify', + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + ], + 'started_by_user_id' => (int) $user->getKey(), + 'updated_by_user_id' => (int) $user->getKey(), + ]); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + ]); + session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); + + visit('/admin/onboarding') + ->assertNoJavaScriptErrors() + ->assertSee('Verify access') + ->assertSee('Run a queued verification check (Operation).') + ->assertSee('Start verification') + ->assertDontSee('Refresh') + ->assertDontSee(OperationRunLinks::openLabel()); +}); + +it('smokes onboarding active verification state with refresh and current-run inspection', function (): void { + $workspace = Workspace::factory()->create(); + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'status' => Tenant::STATUS_ONBOARDING, + ]); + $user = User::factory()->create(['name' => 'Spec172 Active Browser Owner']); + + createUserWithTenant( + tenant: $tenant, + user: $user, + role: 'owner', + workspaceRole: 'owner', + ensureDefaultMicrosoftProviderConnection: false, + ); + + $connection = ProviderConnection::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'provider' => 'microsoft', + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'is_default' => true, + 'status' => 'connected', + ]); + + $run = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => OperationRunStatus::Running->value, + 'outcome' => OperationRunOutcome::Pending->value, + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'target_scope' => [ + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'entra_tenant_name' => (string) $tenant->name, + ], + ], + ]); + + TenantOnboardingSession::query()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'current_step' => 'verify', + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_operation_run_id' => (int) $run->getKey(), + ], + 'started_by_user_id' => (int) $user->getKey(), + 'updated_by_user_id' => (int) $user->getKey(), + ]); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + ]); + session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); + + visit('/admin/onboarding') + ->assertNoJavaScriptErrors() + ->assertSee('Verify access') + ->assertSee('Refresh') + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Start verification'); +}); + +it('smokes onboarding completed verification details with secondary links revealed only in technical details', function (): void { + $workspace = Workspace::factory()->create(); + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => '17217217-2172-4172-9172-172172172172', + 'external_id' => 'browser-spec172-complete', + 'status' => Tenant::STATUS_ONBOARDING, + ]); + $user = User::factory()->create(['name' => 'Spec172 Completed Browser Owner']); + + createUserWithTenant( + tenant: $tenant, + user: $user, + role: 'owner', + workspaceRole: 'owner', + ensureDefaultMicrosoftProviderConnection: false, + ); + + $connection = ProviderConnection::factory()->platform()->consentGranted()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'provider' => 'microsoft', + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'display_name' => 'Spec172 completed connection', + 'is_default' => true, + 'status' => 'connected', + ]); + + $previousRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'target_scope' => [ + 'entra_tenant_id' => (string) $tenant->tenant_id, + ], + 'verification_report' => VerificationReportWriter::build('provider.connection.check', []), + ], + ]); + + $report = VerificationReportWriter::build('provider.connection.check', [ + [ + 'key' => 'permissions.admin_consent', + 'title' => 'Required application permissions', + 'status' => 'fail', + 'severity' => 'critical', + 'blocking' => true, + 'reason_code' => 'permission_denied', + 'message' => 'Missing required Graph permissions.', + 'evidence' => [], + 'next_steps' => [], + ], + ]); + $report['previous_report_id'] = (int) $previousRun->getKey(); + + $currentRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Failed->value, + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'target_scope' => [ + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'entra_tenant_name' => (string) $tenant->name, + ], + 'verification_report' => $report, + ], + ]); + + TenantOnboardingSession::query()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'current_step' => 'verify', + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_operation_run_id' => (int) $currentRun->getKey(), + ], + 'started_by_user_id' => (int) $user->getKey(), + 'updated_by_user_id' => (int) $user->getKey(), + ]); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + ]); + session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); + + $page = visit('/admin/onboarding'); + + $page + ->assertNoJavaScriptErrors() + ->assertSee('Verify access') + ->assertSee('Technical details') + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Open previous operation') + ->assertDontSee(OperationRunLinks::advancedMonitoringLabel()) + ->click('Technical details') + ->waitForText('Verification technical details') + ->assertNoJavaScriptErrors() + ->assertSee('Open previous operation') + ->assertSee(OperationRunLinks::advancedMonitoringLabel()); +}); diff --git a/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php b/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php index 820741cf..7d9c3703 100644 --- a/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php +++ b/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php @@ -4,6 +4,7 @@ use App\Filament\Widgets\Tenant\RecentOperationsSummary; use App\Models\OperationRun; +use App\Support\OperationRunLinks; use Livewire\Livewire; it('renders recent operations from the record tenant in admin panel context', function (): void { @@ -25,6 +26,8 @@ ->assertSee('Provider connection check') ->assertSee('Operation finished') ->assertSee('Open operation') + ->assertSee(OperationRunLinks::openCollectionLabel()) + ->assertSee(OperationRunLinks::collectionScopeDescription()) ->assertSee('No action needed.') ->assertDontSee('No operations yet.'); }); diff --git a/tests/Feature/Filament/TenantVerificationReportWidgetTest.php b/tests/Feature/Filament/TenantVerificationReportWidgetTest.php index 3b72c189..ac00e2c2 100644 --- a/tests/Feature/Filament/TenantVerificationReportWidgetTest.php +++ b/tests/Feature/Filament/TenantVerificationReportWidgetTest.php @@ -111,12 +111,41 @@ Livewire::actingAs($user) ->test(TenantVerificationReport::class) ->assertSee('Provider connection preflight') + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Start verification') ->assertSee(OperationRunLinks::identifierLabel().':') ->assertSee('Read-only:') ->assertSee('Insufficient permission — ask a tenant Owner.'); }); }); +it('keeps existing verification runs inspect-first on the widget surface', function (string $status, string $outcome): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + Filament::setTenant($tenant, true); + + OperationRun::factory()->create([ + 'workspace_id' => (int) $tenant->workspace_id, + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => $status, + 'outcome' => $outcome, + 'context' => [], + ]); + + $component = Livewire::actingAs($user) + ->test(TenantVerificationReport::class, ['record' => $tenant]) + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Start verification') + ->assertSee(ViewTenant::verificationHeaderActionLabel()); + + expect(substr_count($component->html(), OperationRunLinks::openLabel()))->toBe(1) + ->and(substr_count($component->html(), 'Start verification'))->toBe(0); +})->with([ + 'active run' => ['running', 'pending'], + 'completed run' => ['completed', 'blocked'], +]); + it('renders tenant detail without invoking synchronous verification or permission persistence services', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); diff --git a/tests/Feature/Guards/ActionSurfaceContractTest.php b/tests/Feature/Guards/ActionSurfaceContractTest.php index e8858090..a7593209 100644 --- a/tests/Feature/Guards/ActionSurfaceContractTest.php +++ b/tests/Feature/Guards/ActionSurfaceContractTest.php @@ -13,6 +13,7 @@ use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\TenantDiagnostics; use App\Filament\Pages\TenantRequiredPermissions; +use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard; use App\Filament\Resources\AlertDeliveryResource; use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries; use App\Filament\Resources\AlertDestinationResource; @@ -749,8 +750,9 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser ->and($baselineExemptions->hasClass(Alerts::class))->toBeTrue() ->and((string) $baselineExemptions->reasonForClass(Alerts::class))->toContain('cluster entry'); - expect($baselineExemptions->hasClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toBeTrue() - ->and((string) $baselineExemptions->reasonForClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests'); + expect($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue() + ->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests') + ->toContain('spec 172'); expect(method_exists(\App\Filament\System\Pages\Ops\Runbooks::class, 'actionSurfaceDeclaration'))->toBeFalse() ->and($baselineExemptions->hasClass(\App\Filament\System\Pages\Ops\Runbooks::class))->toBeFalse(); @@ -759,6 +761,18 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser ->and($baselineExemptions->hasClass(\App\Filament\System\Pages\RepairWorkspaceOwners::class))->toBeFalse(); }); +it('keeps spec 172 retrofit surfaces covered without broad baseline exemptions', function (): void { + $baselineExemptions = ActionSurfaceExemptions::baseline(); + + expect($baselineExemptions->hasClass(ViewTenant::class))->toBeFalse() + ->and($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue() + ->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class)) + ->toContain('spec 172') + ->toContain('OnboardingVerificationTest') + ->toContain('OnboardingVerificationClustersTest') + ->toContain('OnboardingVerificationV1_5UxTest'); +}); + it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); diff --git a/tests/Feature/Onboarding/OnboardingVerificationClustersTest.php b/tests/Feature/Onboarding/OnboardingVerificationClustersTest.php index 95135b59..30645166 100644 --- a/tests/Feature/Onboarding/OnboardingVerificationClustersTest.php +++ b/tests/Feature/Onboarding/OnboardingVerificationClustersTest.php @@ -119,11 +119,12 @@ ->get('/admin/onboarding') ->assertSuccessful() ->assertSee('Technical details') - ->assertSee(OperationRunLinks::identifierLabel()) - ->assertSee('Open operation details') + ->assertSee(OperationRunLinks::openLabel()) ->assertSee('Required application permissions') ->assertSee('Open required permissions') ->assertSee('Issues') + ->assertDontSee('Open previous operation') + ->assertDontSee('Open operation in Monitoring (advanced)') ->assertSee($entraTenantId); }); @@ -187,7 +188,95 @@ 'onboardingDraft' => (int) $session->getKey(), ]) ->mountAction('wizardVerificationTechnicalDetails') - ->assertSuccessful(); + ->assertMountedActionModalSee('Verification technical details') + ->assertMountedActionModalSee(OperationRunLinks::advancedMonitoringLabel()) + ->assertMountedActionModalSee(OperationRunLinks::identifier((int) $run->getKey())); +}); + +it('keeps previous-run and advanced monitoring links inside technical details only', function (): void { + $workspace = Workspace::factory()->create(); + $user = User::factory()->create(); + + WorkspaceMembership::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'user_id' => (int) $user->getKey(), + 'role' => 'owner', + ]); + + session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); + + $entraTenantId = 'dddddddd-dddd-dddd-dddd-dddddddddddd'; + + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => $entraTenantId, + 'external_id' => 'tenant-clusters-d', + 'status' => 'onboarding', + ]); + + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + $previousReport = VerificationReportWriter::build('provider.connection.check', []); + + $previousRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => 'completed', + 'outcome' => 'succeeded', + 'context' => [ + 'target_scope' => [ + 'entra_tenant_id' => $entraTenantId, + ], + 'verification_report' => $previousReport, + ], + ]); + + $currentReport = VerificationReportWriter::build('provider.connection.check', []); + $currentReport['previous_report_id'] = (int) $previousRun->getKey(); + + $currentRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => 'completed', + 'outcome' => 'failed', + 'context' => [ + 'target_scope' => [ + 'entra_tenant_id' => $entraTenantId, + ], + 'verification_report' => $currentReport, + ], + ]); + + $session = TenantOnboardingSession::query()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'entra_tenant_id' => $entraTenantId, + 'current_step' => 'verify', + 'state' => [ + 'verification_operation_run_id' => (int) $currentRun->getKey(), + ], + 'started_by_user_id' => (int) $user->getKey(), + 'updated_by_user_id' => (int) $user->getKey(), + ]); + + $this->actingAs($user) + ->followingRedirects() + ->get('/admin/onboarding') + ->assertSuccessful() + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Open previous operation') + ->assertDontSee(OperationRunLinks::advancedMonitoringLabel()); + + Livewire::test(ManagedTenantOnboardingWizard::class, [ + 'onboardingDraft' => (int) $session->getKey(), + ]) + ->mountAction('wizardVerificationTechnicalDetails') + ->assertMountedActionModalSee('Open previous operation') + ->assertMountedActionModalSee(OperationRunLinks::advancedMonitoringLabel()); }); it('routes permission-related verification next steps through the required permissions assist', function (): void { diff --git a/tests/Feature/Onboarding/OnboardingVerificationTest.php b/tests/Feature/Onboarding/OnboardingVerificationTest.php index 6182af85..55d6ca45 100644 --- a/tests/Feature/Onboarding/OnboardingVerificationTest.php +++ b/tests/Feature/Onboarding/OnboardingVerificationTest.php @@ -327,13 +327,118 @@ ->get('/admin/onboarding') ->assertSuccessful() ->assertSee('Status: Blocked') - ->assertSee(OperationRunLinks::identifierLabel()) - ->assertSee('Open operation details') + ->assertSee(OperationRunLinks::openLabel()) + ->assertSee('Technical details') + ->assertDontSee('Open operation in Monitoring (advanced)') + ->assertDontSee('Open previous operation') ->assertSee('Missing required Graph permissions.') ->assertSee('Graph permissions') ->assertSee($entraTenantId); }); +it('keeps one onboarding verification path per state while leaving workflow actions on the wizard step', function (): void { + $workspace = Workspace::factory()->create(); + $user = User::factory()->create(); + + WorkspaceMembership::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'user_id' => (int) $user->getKey(), + 'role' => 'owner', + ]); + + session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); + + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'status' => Tenant::STATUS_ONBOARDING, + ]); + + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + $connection = ProviderConnection::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'is_default' => true, + 'status' => 'connected', + 'consent_status' => 'granted', + ]); + + $session = TenantOnboardingSession::query()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'entra_tenant_id' => (string) $tenant->tenant_id, + 'current_step' => 'verify', + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + ], + 'started_by_user_id' => (int) $user->getKey(), + 'updated_by_user_id' => (int) $user->getKey(), + ]); + + $this->actingAs($user) + ->followingRedirects() + ->get('/admin/onboarding') + ->assertSuccessful() + ->assertSee('Start verification') + ->assertSee('Use the workflow action above to start verification for this tenant.') + ->assertDontSee(OperationRunLinks::openLabel()) + ->assertDontSee('Refresh'); + + $activeRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => 'running', + 'outcome' => 'pending', + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + ], + ]); + + $session->forceFill([ + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_operation_run_id' => (int) $activeRun->getKey(), + ], + ])->save(); + + $this->actingAs($user) + ->followingRedirects() + ->get('/admin/onboarding') + ->assertSuccessful() + ->assertSee('Refresh') + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Start verification'); + + $completedRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => 'completed', + 'outcome' => 'succeeded', + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_report' => VerificationReportWriter::build('provider.connection.check', []), + ], + ]); + + $session->forceFill([ + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_operation_run_id' => (int) $completedRun->getKey(), + ], + ])->save(); + + $this->actingAs($user) + ->followingRedirects() + ->get('/admin/onboarding') + ->assertSuccessful() + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Refresh'); +}); + it('clears the stored verification run id when switching provider connections', function (): void { Queue::fake(); diff --git a/tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php b/tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php index 33776578..4daca49f 100644 --- a/tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php +++ b/tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php @@ -10,6 +10,7 @@ use App\Models\VerificationCheckAcknowledgement; use App\Models\Workspace; use App\Models\WorkspaceMembership; +use App\Support\OperationRunLinks; use App\Support\Verification\VerificationReportWriter; use App\Support\Workspaces\WorkspaceContext; @@ -86,7 +87,37 @@ ->get('/admin/onboarding') ->assertSuccessful() ->assertSee('Refresh') + ->assertSee(OperationRunLinks::openLabel()) ->assertDontSee('Start verification'); + + $completedRun = OperationRun::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'tenant_id' => (int) $tenant->getKey(), + 'type' => 'provider.connection.check', + 'status' => 'completed', + 'outcome' => 'succeeded', + 'context' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_report' => VerificationReportWriter::build('provider.connection.check', []), + ], + ]); + + TenantOnboardingSession::query() + ->where('workspace_id', (int) $workspace->getKey()) + ->where('tenant_id', (int) $tenant->getKey()) + ->update([ + 'state' => [ + 'provider_connection_id' => (int) $connection->getKey(), + 'verification_operation_run_id' => (int) $completedRun->getKey(), + ], + ]); + + $this->actingAs($user) + ->followingRedirects() + ->get('/admin/onboarding') + ->assertSuccessful() + ->assertSee(OperationRunLinks::openLabel()) + ->assertDontSee('Refresh'); }); it('orders issues deterministically and groups acknowledged issues', function (): void {