TenantAtlas/tests/Feature/Guards/ActionSurfaceContractTest.php
ahmido 807d574d31 feat: add tenant governance aggregate contract and action surface follow-ups (#199)
## Summary
- amend the operator UI constitution and related SpecKit templates for the new UI/UX governance rules
- add Spec 168 artifacts plus the tenant governance aggregate implementation used by the tenant dashboard, banner, and baseline compare landing surfaces
- normalize Filament action surfaces around clickable-row inspection, grouped secondary actions, and explicit action-surface declarations across enrolled resources and pages
- fix post-suite regressions in membership cache priming, finding workflow state refresh, tenant review derived-state invalidation, and tenant-bound backup-set related navigation

## Commit Series
- `docs: amend operator UI constitution`
- `spec: add tenant governance aggregate contract`
- `feat: add tenant governance aggregate contract`
- `refactor: normalize filament action surfaces`
- `fix: resolve post-suite state regressions`

## Testing
- `vendor/bin/sail artisan test --compact`
- Result: `3176 passed, 8 skipped (17384 assertions)`

## Notes
- Livewire v4 / Filament v5 stack remains unchanged
- no provider registration changes; `bootstrap/providers.php` remains the relevant location
- no new global-search resources or asset-registration changes in this branch

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #199
2026-03-29 21:14:17 +00:00

1710 lines
68 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\InventoryCoverage;
use App\Filament\Pages\Monitoring\Alerts;
use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Pages\NoAccess;
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
use App\Filament\Pages\TenantDiagnostics;
use App\Filament\Pages\TenantRequiredPermissions;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Filament\Resources\AlertDestinationResource;
use App\Filament\Resources\AlertDestinationResource\Pages\ListAlertDestinations;
use App\Filament\Resources\AlertRuleResource;
use App\Filament\Resources\AlertRuleResource\Pages\ListAlertRules;
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupScheduleResource\Pages\EditBackupSchedule;
use App\Filament\Resources\BackupScheduleResource\Pages\ListBackupSchedules;
use App\Filament\Resources\BackupScheduleResource\RelationManagers\BackupScheduleOperationRunsRelationManager;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\BackupSetResource\Pages\EditBackupSet;
use App\Filament\Resources\BackupSetResource\RelationManagers\BackupItemsRelationManager;
use App\Filament\Resources\BaselineProfileResource;
use App\Filament\Resources\BaselineProfileResource\Pages\ListBaselineProfiles;
use App\Filament\Resources\BaselineSnapshotResource;
use App\Filament\Resources\EntraGroupResource;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingExceptionResource\Pages\ListFindingExceptions;
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\FindingResource\Pages\ListFindings;
use App\Filament\Resources\InventoryItemResource;
use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems;
use App\Filament\Resources\OperationRunResource;
use App\Filament\Resources\PolicyResource;
use App\Filament\Resources\PolicyResource\Pages\ListPolicies;
use App\Filament\Resources\PolicyResource\Pages\ViewPolicy;
use App\Filament\Resources\PolicyResource\RelationManagers\VersionsRelationManager;
use App\Filament\Resources\PolicyVersionResource;
use App\Filament\Resources\ProviderConnectionResource;
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
use App\Filament\Resources\RestoreRunResource;
use App\Filament\Resources\RestoreRunResource\Pages\ListRestoreRuns;
use App\Filament\Resources\ReviewPackResource;
use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks;
use App\Filament\Resources\TenantResource;
use App\Filament\Resources\TenantResource\Pages\ManageTenantMemberships;
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Filament\Resources\TenantResource\RelationManagers\TenantMembershipsRelationManager;
use App\Filament\Resources\TenantReviewResource;
use App\Filament\Resources\TenantReviewResource\Pages\ListTenantReviews;
use App\Filament\Resources\Workspaces\Pages\ListWorkspaces;
use App\Filament\Resources\Workspaces\Pages\ViewWorkspace;
use App\Filament\Resources\Workspaces\RelationManagers\WorkspaceMembershipsRelationManager;
use App\Filament\Resources\Workspaces\WorkspaceResource;
use App\Filament\System\Pages\Directory\Tenants as SystemDirectoryTenantsPage;
use App\Filament\System\Pages\Directory\Workspaces as SystemDirectoryWorkspacesPage;
use App\Filament\System\Pages\Ops\Failures as SystemFailuresPage;
use App\Filament\System\Pages\Ops\Runs as SystemRunsPage;
use App\Filament\System\Pages\Ops\Stuck as SystemStuckPage;
use App\Filament\System\Pages\Security\AccessLogs as SystemAccessLogsPage;
use App\Jobs\SyncPoliciesJob;
use App\Models\AlertDelivery;
use App\Models\AlertDestination;
use App\Models\AlertRule;
use App\Models\AuditLog;
use App\Models\BackupItem;
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\BaselineProfile;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ProviderConnection;
use App\Models\RestoreRun;
use App\Models\ReviewPack;
use App\Models\Tenant;
use App\Models\TenantMembership;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\System\SystemDirectoryLinks;
use App\Support\System\SystemOperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
use App\Support\Ui\ActionSurface\ActionSurfaceProfileDefinition;
use App\Support\Ui\ActionSurface\ActionSurfaceValidator;
use App\Support\Ui\ActionSurface\Enums\ActionSurfacePanelScope;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
{
Filament::setCurrentPanel('system');
Filament::setTenant(null, true);
Filament::bootCurrentPanel();
$platformUser = PlatformUser::factory()->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);
});