validate(); expect($result->hasIssues())->toBeFalse($result->formatForAssertion()); }); it('excludes widgets from action surface discovery scope', function (): void { $classes = array_map( static fn ($component): string => $component->className, ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents(), ); $widgetClasses = array_values(array_filter($classes, static function (string $className): bool { return str_starts_with($className, 'App\\Filament\\Widgets\\'); })); expect($widgetClasses)->toBeEmpty(); }); it('keeps baseline exemptions explicit and does not auto-exempt unknown classes', function (): void { $exemptions = ActionSurfaceExemptions::baseline(); expect($exemptions->hasClass('App\\Filament\\Resources\\ActionSurfaceUnknownResource'))->toBeFalse(); }); it('maps tenant/admin panel scope metadata from discovery sources', function (): void { $components = collect(ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents()) ->keyBy('className'); $tenantResource = $components->get(\App\Filament\Resources\TenantResource::class); $policyResource = $components->get(\App\Filament\Resources\PolicyResource::class); expect($tenantResource)->not->toBeNull(); expect($tenantResource?->hasPanelScope(ActionSurfacePanelScope::Admin))->toBeTrue(); expect($policyResource)->not->toBeNull(); expect($policyResource?->hasPanelScope(ActionSurfacePanelScope::Tenant))->toBeTrue(); }); it('requires non-empty reasons for every baseline exemption', function (): void { $reasons = ActionSurfaceExemptions::baseline()->all(); foreach ($reasons as $className => $reason) { expect(trim($reason))->not->toBe('', "Baseline exemption reason is empty for {$className}"); } }); it('discovers the baseline profile resource and validates its declaration', function (): void { $components = collect(ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents()) ->keyBy('className'); $baselineResource = $components->get(BaselineProfileResource::class); expect($baselineResource)->not->toBeNull('BaselineProfileResource should be discovered by action surface discovery'); expect($baselineResource?->hasPanelScope(ActionSurfacePanelScope::Admin))->toBeTrue(); $declaration = BaselineProfileResource::actionSurfaceDeclaration(); $profiles = new ActionSurfaceProfileDefinition; foreach ($profiles->requiredSlots($declaration->profile) as $slot) { expect($declaration->slot($slot)) ->not->toBeNull("Missing required slot {$slot->value} in BaselineProfileResource declaration"); } }); it('keeps BaselineProfile archive under the More menu and declares it in the action surface slots', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $declaration = BaselineProfileResource::actionSurfaceDeclaration(); $details = $declaration->slot(ActionSurfaceSlot::ListRowMoreMenu)?->details; expect($details)->toBeString(); expect($details)->toContain('archive'); $this->actingAs($user); $livewire = Livewire::test(ListBaselineProfiles::class) ->assertCanSeeTableRecords([$profile]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); expect($moreGroup)->toBeInstanceOf(ActionGroup::class); expect($moreGroup?->getLabel())->toBe('More'); $primaryRowActionNames = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toContain('view'); expect($primaryRowActionNames)->not->toContain('archive'); $primaryRowActionCount = count($primaryRowActionNames); expect($primaryRowActionCount)->toBeLessThanOrEqual(2); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($moreActionNames)->toContain('archive'); }); it('ensures representative declarations satisfy required slots', function (): void { $profiles = new ActionSurfaceProfileDefinition; $declarations = [ AlertDeliveryResource::class => AlertDeliveryResource::actionSurfaceDeclaration(), BackupScheduleResource::class => BackupScheduleResource::actionSurfaceDeclaration(), BackupSetResource::class => BackupSetResource::actionSurfaceDeclaration(), BaselineSnapshotResource::class => BaselineSnapshotResource::actionSurfaceDeclaration(), EntraGroupResource::class => EntraGroupResource::actionSurfaceDeclaration(), EvidenceSnapshotResource::class => EvidenceSnapshotResource::actionSurfaceDeclaration(), PolicyResource::class => PolicyResource::actionSurfaceDeclaration(), OperationRunResource::class => OperationRunResource::actionSurfaceDeclaration(), VersionsRelationManager::class => VersionsRelationManager::actionSurfaceDeclaration(), BaselineProfileResource::class => BaselineProfileResource::actionSurfaceDeclaration(), WorkspaceResource::class => WorkspaceResource::actionSurfaceDeclaration(), ]; foreach ($declarations as $className => $declaration) { foreach ($profiles->requiredSlots($declaration->profile) as $slot) { expect($declaration->slot($slot)) ->not->toBeNull("Missing required slot {$slot->value} in declaration for {$className}"); } } }); it('requires every first-slice tenant-owned resource to be discovered without relying on baseline action-surface exemptions', function (): void { $components = collect(ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents()) ->keyBy('className'); $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach (TenantOwnedModelFamilies::firstSlice() as $familyName => $family) { $resourceClass = $family['resource']; expect($components->has($resourceClass)) ->toBeTrue("{$familyName} resource should be discoverable by the action-surface validator."); $hasDeclaration = method_exists($resourceClass, 'actionSurfaceDeclaration'); $hasBaselineExemption = $baselineExemptions->hasClass($resourceClass); expect($hasDeclaration || $hasBaselineExemption) ->toBeTrue("{$familyName} resource must either define actionSurfaceDeclaration() or carry an explicit baseline exemption."); if ($hasDeclaration) { expect($hasBaselineExemption) ->toBeFalse("{$familyName} resource should not keep a stale baseline exemption once actionSurfaceDeclaration() exists."); continue; } expect(trim((string) $baselineExemptions->reasonForClass($resourceClass))) ->not->toBe('', "{$familyName} resource baseline exemption reason must stay explicit."); } }); it('keeps first-slice tenant-owned action-surface exemptions registry-backed and explicit', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach (TenantOwnedModelFamilies::actionSurfaceBaselineExemptions() as $className => $reason) { expect($baselineExemptions->reasonForClass($className)) ->toBe($reason); } foreach (TenantOwnedModelFamilies::firstSlice() as $familyName => $family) { if ($family['action_surface'] !== 'baseline_exemption') { continue; } expect(trim($family['action_surface_reason'])) ->not->toBe('', "{$familyName} baseline exemption reason must stay explicit in the registry."); } }); it('keeps first-slice trusted-state page action-surface status explicit', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); expect($baselineExemptions->hasClass(\App\Filament\Pages\TenantRequiredPermissions::class))->toBeTrue() ->and((string) $baselineExemptions->reasonForClass(\App\Filament\Pages\TenantRequiredPermissions::class))->toContain('dedicated tests'); 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(method_exists(\App\Filament\System\Pages\Ops\Runbooks::class, 'actionSurfaceDeclaration'))->toBeFalse() ->and($baselineExemptions->hasClass(\App\Filament\System\Pages\Ops\Runbooks::class))->toBeFalse(); }); it('documents the guided alert delivery empty state without introducing a list-header CTA', function (): void { $declaration = AlertDeliveryResource::actionSurfaceDeclaration(); expect((string) ($declaration->slot(ActionSurfaceSlot::ListEmptyState)?->details ?? '')) ->toContain('View alert rules'); }); it('uses More grouping conventions and exposes empty-state CTA on representative CRUD list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListPolicies::class) ->assertTableEmptyStateActionsExistInOrder(['sync']); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $rowGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); expect($rowGroup)->toBeInstanceOf(ActionGroup::class); expect($rowGroup?->getLabel())->toBe('More'); $primaryRowActionCount = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->count(); expect($primaryRowActionCount)->toBeLessThanOrEqual(2); $bulkActions = $table->getBulkActions(); $bulkGroup = collect($bulkActions)->first(static fn ($action): bool => $action instanceof BulkActionGroup); expect($bulkGroup)->toBeInstanceOf(BulkActionGroup::class); expect($bulkGroup?->getLabel())->toBe('More'); }); it('keeps evidence snapshots on the declared clickable-row, two-action surface', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListEvidenceSnapshots::class) ->assertTableEmptyStateActionsExistInOrder(['create_first_snapshot']); $snapshot = EvidenceSnapshot::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'status' => EvidenceSnapshotStatus::Active->value, 'completeness_state' => EvidenceCompletenessState::Complete->value, 'summary' => ['finding_count' => 1, 'missing_dimensions' => 0], 'generated_at' => now(), ]); $livewire = Livewire::test(ListEvidenceSnapshots::class) ->assertCanSeeTableRecords([$snapshot]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $primaryRowActionNames = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toBe(['view_snapshot', 'expire']); expect($table->getBulkActions())->toBeEmpty(); expect($table->getRecordUrl($snapshot))->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot])); }); it('uses canonical tenantless View run links on representative operation links', function (): void { $tenant = Tenant::factory()->create(); $run = OperationRun::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, ]); expect(OperationRunLinks::view($run, $tenant)) ->toBe(route('admin.operations.view', ['run' => (int) $run->getKey()])); }); it('removes lone View buttons and uses clickable rows on the inventory items list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), ]); $livewire = Livewire::test(ListInventoryItems::class); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty(); $recordUrl = $table->getRecordUrl($item); expect($recordUrl)->not->toBeNull(); expect($recordUrl)->toBe(InventoryItemResource::getUrl('view', ['record' => $item])); }); it('keeps representative operation-start actions observable with actor and scope metadata', function (): void { Queue::fake(); bindFailHardGraphClient(); [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->mountAction('sync') ->callMountedAction() ->assertHasNoActionErrors(); Queue::assertPushed(SyncPoliciesJob::class); $run = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'policy.sync') ->latest('id') ->first(); expect($run)->not->toBeNull(); expect((int) $run?->tenant_id)->toBe((int) $tenant->getKey()); expect((int) $run?->workspace_id)->toBe((int) $tenant->workspace_id); expect((string) $run?->initiator_name)->toBe((string) $user->name); });