create([ 'capabilities' => array_values(array_unique(array_merge([ PlatformCapabilities::ACCESS_SYSTEM_PANEL, ], $capabilities))), 'is_active' => true, ]); test()->actingAs($platformUser, 'platform'); return $platformUser; } it('passes the action surface contract guard for current repository state', function (): void { $result = ActionSurfaceValidator::withBaselineExemptions()->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)->toBe([]) ->and($table->getRecordUrl($profile))->toBe(BaselineProfileResource::getUrl('view', ['record' => $profile])); $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'); expect($table->getBulkActions())->toBeEmpty(); }); it('keeps backup schedules on clickable-row edit without duplicate Edit actions in More', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $schedule = BackupSchedule::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'name' => 'Nightly backup', 'is_enabled' => true, 'timezone' => 'UTC', 'frequency' => 'daily', 'time_of_day' => '01:00:00', 'days_of_week' => null, 'policy_types' => ['deviceConfiguration'], 'include_foundations' => true, 'retention_keep_last' => 30, ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListBackupSchedules::class) ->assertCanSeeTableRecords([$schedule]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); expect($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($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)->toBe([]) ->and($table->getRecordUrl($schedule))->toBe(BackupScheduleResource::getUrl('edit', ['record' => $schedule])); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($moreActionNames)->toContain('runNow', 'retry', 'archive') ->and($moreActionNames)->not->toContain('edit'); $bulkActions = $table->getBulkActions(); $bulkGroup = collect($bulkActions)->first(static fn ($action): bool => $action instanceof BulkActionGroup); expect($bulkGroup)->toBeInstanceOf(BulkActionGroup::class) ->and($bulkGroup?->getLabel())->toBe('More'); $bulkActionNames = collect($bulkGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($bulkActionNames)->toEqualCanonicalizing(['bulk_run_now', 'bulk_retry']); }); it('uses clickable rows without extra row actions on backup schedule executions', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $schedule = BackupSchedule::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'name' => 'Nightly backup', 'is_enabled' => true, 'timezone' => 'UTC', 'frequency' => 'daily', 'time_of_day' => '01:00:00', 'days_of_week' => null, 'policy_types' => ['deviceConfiguration'], 'include_foundations' => true, 'retention_keep_last' => 30, ]); $run = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'backup_schedule_run', 'context' => ['backup_schedule_id' => (int) $schedule->getKey()], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(BackupScheduleOperationRunsRelationManager::class, [ 'ownerRecord' => $schedule, 'pageClass' => EditBackupSchedule::class, ]) ->assertCanSeeTableRecords([$run]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($run))->toBe(OperationRunLinks::view($run, $tenant)); }); it('uses clickable rows while keeping remove grouped under More on backup items', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), ]); $version = PolicyVersion::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'policy_id' => (int) $policy->getKey(), 'metadata' => [], ]); $backupSet = BackupSet::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), ]); $backupItem = BackupItem::factory()->for($backupSet)->for($tenant)->create([ 'policy_id' => (int) $policy->getKey(), 'policy_version_id' => (int) $version->getKey(), 'metadata' => [], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(BackupItemsRelationManager::class, [ 'ownerRecord' => $backupSet, 'pageClass' => EditBackupSet::class, ]) ->assertCanSeeTableRecords([$backupItem]); $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(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $bulkActions = $table->getBulkActions(); $bulkGroup = collect($bulkActions)->first(static fn ($action): bool => $action instanceof BulkActionGroup); $bulkActionNames = collect($bulkGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toBe([]) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['remove']) ->and($bulkGroup)->toBeInstanceOf(BulkActionGroup::class) ->and($bulkGroup?->getLabel())->toBe('More') ->and($bulkActionNames)->toEqualCanonicalizing(['bulk_remove']) ->and($table->getRecordUrl($backupItem))->toBe(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); }); it('keeps tenant memberships inline without a separate inspect affordance', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $member = User::factory()->create(); $member->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'readonly'], ]); $membership = TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $member->getKey()) ->firstOrFail(); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(TenantMembershipsRelationManager::class, [ 'ownerRecord' => $tenant, 'pageClass' => ViewTenant::class, ]) ->assertCanSeeTableRecords([$membership]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['change_role', 'remove']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($membership))->toBeNull(); }); it('keeps workspace memberships inline without a separate inspect affordance', function (): void { $workspace = Workspace::factory()->create(); $owner = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $owner->getKey(), 'role' => 'owner', ]); $member = User::factory()->create(); $membership = WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $member->getKey(), 'role' => 'readonly', ]); $this->actingAs($owner); $livewire = Livewire::test(WorkspaceMembershipsRelationManager::class, [ 'ownerRecord' => $workspace, 'pageClass' => ViewWorkspace::class, ]) ->assertCanSeeTableRecords([$membership]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $moreGroup = collect($table->getActions()) ->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions() ?? []) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['change_role']) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['remove']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($membership))->toBeNull(); }); it('renders the policy versions relation manager on the policy detail page', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), ]); PolicyVersion::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'policy_id' => (int) $policy->getKey(), 'created_by' => 'versions-surface@example.test', 'metadata' => [], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $this->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)) ->assertOk() ->assertSee('Versions'); }); it('renders tenant memberships only on the dedicated memberships page', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $member = User::factory()->create([ 'email' => 'tenant-members-surface@example.test', ]); $member->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'readonly'], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(TenantResource::getUrl('view', ['record' => $tenant->getRouteKey()], panel: 'admin')) ->assertOk() ->assertDontSeeLivewire(TenantMembershipsRelationManager::class); session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]); $membershipsPage = Livewire::actingAs($user) ->test(ManageTenantMemberships::class, ['record' => $tenant->getRouteKey()]); expect($membershipsPage->instance()->getRelationManagers()) ->toContain(TenantMembershipsRelationManager::class); $this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(TenantResource::getUrl('memberships', ['record' => $tenant->getRouteKey()], panel: 'admin')) ->assertOk() ->assertSeeLivewire(TenantMembershipsRelationManager::class); }); it('renders the backup items relation manager on the backup set detail page', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'display_name' => 'Backup Items Surface Policy', ]); $backupSet = BackupSet::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), ]); BackupItem::factory()->for($backupSet)->for($tenant)->create([ 'policy_id' => (int) $policy->getKey(), 'policy_version_id' => null, 'metadata' => [], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $this->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant)) ->assertOk() ->assertSee('Items'); }); it('renders the workspace memberships relation manager on the workspace detail page', function (): void { $workspace = Workspace::factory()->create(); $owner = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $owner->getKey(), 'role' => 'owner', ]); $member = User::factory()->create([ 'email' => 'workspace-members-surface@example.test', ]); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $member->getKey(), 'role' => 'readonly', ]); $this->actingAs($owner); $this->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(WorkspaceResource::getUrl('view', ['record' => $workspace])) ->assertOk() ->assertSee('Memberships'); }); it('keeps inventory coverage as derived metadata without inspect or row action affordances', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $conditionalAccessKey = 'policy:conditionalAccessPolicy'; $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(InventoryCoverage::class) ->assertCanSeeTableRecords([$conditionalAccessKey]) ->assertTableEmptyStateActionsExistInOrder(['clear_filters']); $table = $livewire->instance()->getTable(); $declaration = InventoryCoverage::actionSurfaceDeclaration(); expect($table->getActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty() ->and($table->getEmptyStateActions())->toHaveCount(1) ->and((string) ($declaration->exemption(ActionSurfaceSlot::InspectAffordance)?->reason ?? '')) ->toContain('runtime-derived metadata'); }); it('ensures representative declarations satisfy required slots', function (): void { $profiles = new ActionSurfaceProfileDefinition; $declarations = [ InventoryCoverage::class => InventoryCoverage::actionSurfaceDeclaration(), NoAccess::class => NoAccess::actionSurfaceDeclaration(), TenantlessOperationRunViewer::class => TenantlessOperationRunViewer::actionSurfaceDeclaration(), TenantDiagnostics::class => TenantDiagnostics::actionSurfaceDeclaration(), TenantRequiredPermissions::class => TenantRequiredPermissions::actionSurfaceDeclaration(), AlertDeliveryResource::class => AlertDeliveryResource::actionSurfaceDeclaration(), BackupScheduleResource::class => BackupScheduleResource::actionSurfaceDeclaration(), BackupScheduleOperationRunsRelationManager::class => BackupScheduleOperationRunsRelationManager::actionSurfaceDeclaration(), BackupSetResource::class => BackupSetResource::actionSurfaceDeclaration(), BackupItemsRelationManager::class => BackupItemsRelationManager::actionSurfaceDeclaration(), BaselineSnapshotResource::class => BaselineSnapshotResource::actionSurfaceDeclaration(), EntraGroupResource::class => EntraGroupResource::actionSurfaceDeclaration(), EvidenceSnapshotResource::class => EvidenceSnapshotResource::actionSurfaceDeclaration(), FindingExceptionResource::class => FindingExceptionResource::actionSurfaceDeclaration(), Operations::class => Operations::actionSurfaceDeclaration(), PolicyResource::class => PolicyResource::actionSurfaceDeclaration(), OperationRunResource::class => OperationRunResource::actionSurfaceDeclaration(), ReviewPackResource::class => ReviewPackResource::actionSurfaceDeclaration(), RestoreRunResource::class => RestoreRunResource::actionSurfaceDeclaration(), TenantMembershipsRelationManager::class => TenantMembershipsRelationManager::actionSurfaceDeclaration(), VersionsRelationManager::class => VersionsRelationManager::actionSurfaceDeclaration(), BaselineProfileResource::class => BaselineProfileResource::actionSurfaceDeclaration(), WorkspaceMembershipsRelationManager::class => WorkspaceMembershipsRelationManager::actionSurfaceDeclaration(), WorkspaceResource::class => WorkspaceResource::actionSurfaceDeclaration(), SystemRunsPage::class => SystemRunsPage::actionSurfaceDeclaration(), SystemFailuresPage::class => SystemFailuresPage::actionSurfaceDeclaration(), SystemStuckPage::class => SystemStuckPage::actionSurfaceDeclaration(), SystemDirectoryTenantsPage::class => SystemDirectoryTenantsPage::actionSurfaceDeclaration(), SystemDirectoryWorkspacesPage::class => SystemDirectoryWorkspacesPage::actionSurfaceDeclaration(), SystemAccessLogsPage::class => SystemAccessLogsPage::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(); $registeredExemptions = TenantOwnedModelFamilies::actionSurfaceBaselineExemptions(); $declaredExemptions = collect(TenantOwnedModelFamilies::firstSlice()) ->filter(static fn (array $family): bool => $family['action_surface'] === 'baseline_exemption') ->mapWithKeys(static fn (array $family): array => [$family['resource'] => $family['action_surface_reason']]) ->all(); expect($registeredExemptions)->toBe($declaredExemptions); foreach ($registeredExemptions 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(method_exists(TenantRequiredPermissions::class, 'actionSurfaceDeclaration'))->toBeTrue() ->and($baselineExemptions->hasClass(TenantRequiredPermissions::class))->toBeFalse(); expect(method_exists(Alerts::class, 'actionSurfaceDeclaration'))->toBeFalse() ->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(method_exists(\App\Filament\System\Pages\Ops\Runbooks::class, 'actionSurfaceDeclaration'))->toBeFalse() ->and($baselineExemptions->hasClass(\App\Filament\System\Pages\Ops\Runbooks::class))->toBeFalse(); }); it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ SystemRunsPage::class, SystemFailuresPage::class, SystemStuckPage::class, SystemDirectoryTenantsPage::class, SystemDirectoryWorkspacesPage::class, SystemAccessLogsPage::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled relation managers declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ BackupItemsRelationManager::class, TenantMembershipsRelationManager::class, WorkspaceMembershipsRelationManager::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled monitoring pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ Operations::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled tenant table pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ InventoryCoverage::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled canonical detail pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ TenantlessOperationRunViewer::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled singleton tenant pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ NoAccess::class, TenantDiagnostics::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps enrolled guided workspace diagnostic pages declaration-backed without stale baseline exemptions', function (): void { $baselineExemptions = ActionSurfaceExemptions::baseline(); foreach ([ TenantRequiredPermissions::class, ] as $className) { expect(method_exists($className, 'actionSurfaceDeclaration')) ->toBeTrue("{$className} should declare its action surface once enrolled."); expect($baselineExemptions->hasClass($className)) ->toBeFalse("{$className} should not keep a stale baseline exemption after enrollment."); } }); it('keeps finding exception v1 list exemptions explicit and omits grouped or bulk mutations', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $declaration = FindingExceptionResource::actionSurfaceDeclaration(); expect((string) ($declaration->exemption(ActionSurfaceSlot::ListRowMoreMenu)?->reason ?? '')) ->toContain('avoids a More menu'); expect((string) ($declaration->exemption(ActionSurfaceSlot::ListBulkMoreGroup)?->reason ?? '')) ->toContain('omit bulk actions'); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListFindingExceptions::class) ->assertTableEmptyStateActionsExistInOrder(['open_findings']); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); expect(collect($rowActions)->contains(static fn ($action): bool => $action instanceof ActionGroup))->toBeFalse(); expect(collect($rowActions)->map(static fn ($action): ?string => $action->getName())->filter()->values()->all()) ->toEqualCanonicalizing(['renew_exception', 'revoke_exception']); expect($table->getBulkActions())->toBeEmpty(); }); 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(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toBe([]) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['expire']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($snapshot))->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot])); }); it('uses clickable rows without a duplicate View action on the tenant reviews list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $review = composeTenantReviewForTest($tenant, $user); $this->actingAs($user); setTenantPanelContext($tenant); $livewire = Livewire::test(ListTenantReviews::class) ->assertCanSeeTableRecords([$review]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['export_executive_pack']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($review))->toBe(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)); }); it('uses clickable rows while keeping direct download and expire shortcuts on the review packs list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $pack = ReviewPack::factory()->ready()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListReviewPacks::class) ->assertCanSeeTableRecords([$pack]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['download', 'expire']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($pack))->toBe(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant)); }); it('uses clickable rows while grouping restore-run maintenance actions under More', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $backupSet = BackupSet::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'status' => 'completed', ]); $restoreRun = RestoreRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), 'status' => 'completed', 'deleted_at' => null, ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListRestoreRuns::class) ->assertCanSeeTableRecords([$restoreRun]); $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(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $bulkActions = $table->getBulkActions(); $bulkGroup = collect($bulkActions)->first(static fn ($action): bool => $action instanceof BulkActionGroup); $bulkActionNames = collect($bulkGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toBe([]) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['rerun', 'restore', 'archive', 'forceDelete']) ->and($bulkGroup)->toBeInstanceOf(BulkActionGroup::class) ->and($bulkGroup?->getLabel())->toBe('More') ->and($bulkActionNames)->toEqualCanonicalizing(['bulk_delete', 'bulk_restore', 'bulk_force_delete']) ->and($table->getRecordUrl($restoreRun))->toBe(RestoreRunResource::getUrl('view', ['record' => $restoreRun], tenant: $tenant)); }); it('keeps findings on clickable-row inspection with a single related drill-down and grouped workflow actions', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $finding = Finding::factory()->for($tenant)->create([ 'status' => Finding::STATUS_NEW, ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListFindings::class) ->assertCanSeeTableRecords([$finding]); $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(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $bulkActions = $table->getBulkActions(); $bulkGroup = collect($bulkActions)->first(static fn ($action): bool => $action instanceof BulkActionGroup); $bulkActionNames = collect($bulkGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($primaryRowActionNames)->toEqualCanonicalizing(['primary_drill_down']) ->and($table->getRecordUrl($finding))->toBe(FindingResource::getUrl('view', ['record' => $finding])) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing([ 'triage', 'start_progress', 'assign', 'resolve', 'close', 'request_exception', 'renew_exception', 'revoke_exception', 'reopen', ]) ->and($bulkGroup)->toBeInstanceOf(BulkActionGroup::class) ->and($bulkGroup?->getLabel())->toBe('More') ->and($bulkActionNames)->toEqualCanonicalizing([ 'triage_selected', 'assign_selected', 'resolve_selected', 'close_selected', ]); }); it('uses clickable rows with restore as the only inline shortcut on the policy versions relation manager', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), ]); $version = PolicyVersion::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'policy_id' => (int) $policy->getKey(), 'metadata' => [], ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(VersionsRelationManager::class, [ 'ownerRecord' => $policy, 'pageClass' => ViewPolicy::class, ]) ->assertCanSeeTableRecords([$version]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['restore_to_intune']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($version))->toBe(PolicyVersionResource::getUrl('view', ['record' => $version])); }); 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('uses clickable rows without a lone View action on the monitoring operations list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'policy.sync', 'status' => 'queued', 'outcome' => 'pending', ]); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); Filament::setTenant($tenant, true); $livewire = Livewire::test(Operations::class) ->assertCanSeeTableRecords([$run]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getRecordUrl($run))->toBe(OperationRunLinks::tenantlessView($run)); }); it('keeps tenantless run detail header actions on the canonical viewer without list affordances', 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', ]); $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => null, 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, ]); session()->forget(WorkspaceContext::SESSION_KEY); Livewire::actingAs($user)->test(TenantlessOperationRunViewer::class, ['run' => $run]) ->assertActionVisible('operate_hub_scope_run_detail') ->assertActionVisible('operate_hub_back_to_operations') ->assertActionVisible('refresh'); $declaration = TenantlessOperationRunViewer::actionSurfaceDeclaration(); expect((string) ($declaration->exemption(ActionSurfaceSlot::InspectAffordance)?->reason ?? '')) ->toContain('canonical detail destination') ->and((string) ($declaration->slot(ActionSurfaceSlot::DetailHeader)?->details ?? '')) ->toContain('refresh'); }); it('keeps tenant diagnostics as a singleton repair surface with header actions only', function (): void { [$manager, $tenant] = createUserWithTenant(role: 'manager'); $this->actingAs($manager); Filament::setTenant($tenant, true); Livewire::test(TenantDiagnostics::class) ->assertActionVisible('bootstrapOwner') ->assertActionEnabled('bootstrapOwner'); $declaration = TenantDiagnostics::actionSurfaceDeclaration(); expect((string) ($declaration->slot(ActionSurfaceSlot::ListHeader)?->details ?? '')) ->toContain('repair actions') ->and((string) ($declaration->exemption(ActionSurfaceSlot::InspectAffordance)?->reason ?? '')) ->toContain('singleton diagnostic surface'); }); it('keeps the no-access page as a singleton recovery surface with a header action', function (): void { $user = User::factory()->create(); $this->actingAs($user); Livewire::test(NoAccess::class) ->assertActionVisible('createWorkspace') ->assertActionEnabled('createWorkspace'); $declaration = NoAccess::actionSurfaceDeclaration(); expect((string) ($declaration->slot(ActionSurfaceSlot::ListHeader)?->details ?? '')) ->toContain('create-workspace recovery action') ->and((string) ($declaration->exemption(ActionSurfaceSlot::InspectAffordance)?->reason ?? '')) ->toContain('singleton recovery surface'); }); it('keeps required permissions as a guided diagnostic page with inline filters and empty-state guidance', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly'); $response = $this->actingAs($user) ->get("/admin/tenants/{$tenant->external_id}/required-permissions"); $response->assertOk() ->assertSee('Copy missing application permissions') ->assertSee('Copy missing delegated permissions') ->assertSee('Re-run verification') ->assertSee('Start verification'); $declaration = TenantRequiredPermissions::actionSurfaceDeclaration(); expect((string) ($declaration->exemption(ActionSurfaceSlot::ListHeader)?->reason ?? '')) ->toContain('body sections') ->and((string) ($declaration->slot(ActionSurfaceSlot::ListEmptyState)?->details ?? '')) ->toContain('no-data'); }); it('uses clickable rows with direct triage actions on the system runs list', function (): void { $run = OperationRun::factory()->create([ 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'type' => 'inventory_sync', ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::OPERATIONS_VIEW, PlatformCapabilities::OPERATIONS_MANAGE, ]); $livewire = Livewire::test(SystemRunsPage::class) ->assertCanSeeTableRecords([$run]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); }); it('uses clickable rows with direct triage actions on the system failures list', function (): void { $run = OperationRun::factory()->create([ 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'type' => 'inventory_sync', ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::OPERATIONS_VIEW, PlatformCapabilities::OPERATIONS_MANAGE, ]); $livewire = Livewire::test(SystemFailuresPage::class) ->assertCanSeeTableRecords([$run]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); }); it('uses clickable rows with direct triage actions on the system stuck list', function (): void { $run = OperationRun::factory()->create([ 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subHours(2), 'started_at' => null, 'type' => 'inventory_sync', ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::OPERATIONS_VIEW, PlatformCapabilities::OPERATIONS_MANAGE, ]); $livewire = Livewire::test(SystemStuckPage::class) ->assertCanSeeTableRecords([$run]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); }); it('uses clickable rows without extra row actions on the system tenants directory', function (): void { $workspace = Workspace::factory()->create([ 'name' => 'System Directory Workspace', ]); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'System Directory Tenant', ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::DIRECTORY_VIEW, ]); $livewire = Livewire::test(SystemDirectoryTenantsPage::class) ->assertCanSeeTableRecords([$tenant]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($tenant))->toBe(SystemDirectoryLinks::tenantDetail($tenant)); }); it('uses clickable rows without extra row actions on the system workspaces directory', function (): void { $workspace = Workspace::factory()->create([ 'name' => 'System Directory Workspace', ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::DIRECTORY_VIEW, ]); $livewire = Livewire::test(SystemDirectoryWorkspacesPage::class) ->assertCanSeeTableRecords([$workspace]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($workspace))->toBe(SystemDirectoryLinks::workspaceDetail($workspace)); }); it('keeps system access logs scan-only without row or bulk actions', function (): void { $tenant = Tenant::factory()->create(); $log = AuditLog::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'action' => 'platform.auth.login', 'status' => 'success', 'metadata' => ['attempted_email' => 'operator@tenantpilot.test'], 'recorded_at' => now(), ]); actionSurfaceSystemPanelContext([ PlatformCapabilities::CONSOLE_VIEW, ]); $livewire = Livewire::test(SystemAccessLogsPage::class) ->assertCanSeeTableRecords([$log]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty() ->and($table->getRecordUrl($log))->toBeNull(); }); 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('uses clickable rows without a lone View action on the workspaces list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); Filament::setTenant(null, true); $workspace = $tenant->workspace; $livewire = Livewire::test(ListWorkspaces::class) ->assertCanSeeTableRecords([$workspace]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toContain('edit') ->and($rowActionNames)->not->toContain('view') ->and($table->getRecordUrl($workspace))->toBe(WorkspaceResource::getUrl('view', ['record' => $workspace])); }); it('uses clickable rows without a lone View action on the policies list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'external_id' => 'policy-action-surface-1', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Policy Action Surface', 'platform' => 'windows', 'last_synced_at' => now(), ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListPolicies::class) ->assertCanSeeTableRecords([$policy]); $table = $livewire->instance()->getTable(); $rowActionNames = collect($table->getActions()) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->not->toContain('view') ->and($table->getRecordUrl($policy))->toBe(PolicyResource::getUrl('view', ['record' => $policy])); }); it('uses clickable rows without a duplicate Edit action on the alert rules list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) $tenant->workspace_id; $rule = AlertRule::factory()->create([ 'workspace_id' => $workspaceId, ]); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); Filament::setTenant(null, true); $livewire = Livewire::test(ListAlertRules::class) ->assertCanSeeTableRecords([$rule]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $rowActionNames = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->not->toContain('edit') ->and($table->getRecordUrl($rule))->toBe(AlertRuleResource::getUrl('edit', ['record' => $rule])) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['toggle_enabled', 'delete']) ->and($table->getBulkActions())->toBeEmpty(); }); it('uses clickable rows without a duplicate Edit action on the alert destinations list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) $tenant->workspace_id; $destination = AlertDestination::factory()->create([ 'workspace_id' => $workspaceId, ]); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); Filament::setTenant(null, true); $livewire = Livewire::test(ListAlertDestinations::class) ->assertCanSeeTableRecords([$destination]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $rowActionNames = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->not->toContain('edit') ->and($table->getRecordUrl($destination))->toBe(AlertDestinationResource::getUrl('edit', ['record' => $destination])) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing(['toggle_enabled', 'delete']) ->and($table->getBulkActions())->toBeEmpty(); }); it('uses clickable-row view with all secondary provider connection actions grouped under More', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner', ensureDefaultMicrosoftProviderConnection: false); $connection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'provider' => 'microsoft', 'status' => 'connected', 'is_default' => false, ]); $this->actingAs($user); $tenant->makeCurrent(); Filament::setTenant($tenant, true); $livewire = Livewire::test(ListProviderConnections::class) ->assertCanSeeTableRecords([$connection]); $table = $livewire->instance()->getTable(); $rowActions = $table->getActions(); $rowActionNames = collect($rowActions) ->reject(static fn ($action): bool => $action instanceof ActionGroup) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); $moreGroup = collect($rowActions)->first(static fn ($action): bool => $action instanceof ActionGroup); $moreActionNames = collect($moreGroup?->getActions()) ->map(static fn ($action): ?string => $action->getName()) ->filter() ->values() ->all(); expect($rowActionNames)->toBeEmpty() ->and($table->getRecordUrl($connection))->toBe(ProviderConnectionResource::getUrl('view', ['record' => $connection], tenant: $tenant)) ->and($moreGroup)->toBeInstanceOf(ActionGroup::class) ->and($moreGroup?->getLabel())->toBe('More') ->and($moreActionNames)->toEqualCanonicalizing([ 'edit', 'check_connection', 'inventory_sync', 'compliance_snapshot', 'set_default', 'enable_dedicated_override', 'rotate_dedicated_credential', 'delete_dedicated_credential', 'revert_to_platform', 'enable_connection', 'disable_connection', ]) ->and($table->getBulkActions())->toBeEmpty(); }); it('uses clickable rows without extra row actions on the alert deliveries list', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) $tenant->workspace_id; $rule = AlertRule::factory()->create([ 'workspace_id' => $workspaceId, ]); $destination = AlertDestination::factory()->create([ 'workspace_id' => $workspaceId, ]); $delivery = AlertDelivery::factory()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'alert_rule_id' => (int) $rule->getKey(), 'alert_destination_id' => (int) $destination->getKey(), ]); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); Filament::setTenant(null, true); $livewire = Livewire::test(ListAlertDeliveries::class) ->assertCanSeeTableRecords([$delivery]); $table = $livewire->instance()->getTable(); expect($table->getActions())->toBeEmpty() ->and($table->getRecordUrl($delivery))->toBe(AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin')); }); 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); });