feat: cut over workspace-owned analysis shell context
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m49s

This commit is contained in:
Ahmed Darrazi 2026-05-17 00:22:17 +02:00
parent ddf7c15c52
commit ef9380ac32
17 changed files with 399 additions and 114 deletions

View File

@ -15,6 +15,7 @@
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperateHub\OperateHubShell;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
@ -182,7 +183,7 @@ public function availableFilters(): array
'options' => [],
],
[
'key' => 'tenant',
'key' => 'environment_id',
'label' => 'ManagedEnvironment',
'fixed' => false,
'options' => collect($this->visibleTenants())
@ -402,14 +403,22 @@ private function tenantFilterOptions(): array
private function applyRequestedTenantPrefilter(): void
{
$requestedTenant = request()->query('tenant');
$workspace = $this->workspace();
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
if (! $workspace instanceof Workspace) {
return;
}
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
return;
}
$environmentId = $filter->environmentId();
foreach ($this->visibleTenants() as $tenant) {
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
if ((int) $tenant->getKey() !== $environmentId) {
continue;
}
@ -418,6 +427,8 @@ private function applyRequestedTenantPrefilter(): void
return;
}
throw new NotFoundHttpException;
}
private function normalizeTenantFilterState(): void
@ -583,9 +594,9 @@ private function navigationContext(): CanonicalNavigationContext
private function reportUrl(array $overrides = []): string
{
$resolvedTenant = array_key_exists('tenant', $overrides)
? $overrides['tenant']
: $this->filteredTenant()?->external_id;
$resolvedEnvironment = array_key_exists('environment_id', $overrides)
? $overrides['environment_id']
: $this->filteredTenant()?->getKey();
$resolvedReason = array_key_exists('reason', $overrides)
? $overrides['reason']
: $this->currentReasonFilter();
@ -593,7 +604,7 @@ private function reportUrl(array $overrides = []): string
return static::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => is_string($resolvedTenant) && $resolvedTenant !== '' ? $resolvedTenant : null,
'environment_id' => is_numeric($resolvedEnvironment) ? (int) $resolvedEnvironment : null,
'reason' => is_string($resolvedReason) && $resolvedReason !== FindingAssignmentHygieneService::FILTER_ALL
? $resolvedReason
: null,

View File

@ -19,6 +19,7 @@
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperateHub\OperateHubShell;
use App\Support\Rbac\UiEnforcement;
use App\Support\Rbac\UiTooltips;
@ -79,7 +80,7 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
->withListRowPrimaryActionLimit(1)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the shared-unassigned scope fixed and expose only a tenant-prefilter clear action when needed.')
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the shared-unassigned scope fixed and expose only an environment-prefilter clear action when needed.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The intake queue keeps Claim finding inline and does not render a secondary More menu on rows.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The intake queue does not expose bulk actions in v1.')
@ -521,14 +522,22 @@ private function tenantFilterOptions(): array
private function applyRequestedTenantPrefilter(): void
{
$requestedTenant = request()->query('tenant');
$workspace = $this->workspace();
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
if (! $workspace instanceof Workspace) {
return;
}
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
return;
}
$environmentId = $filter->environmentId();
foreach ($this->visibleTenants() as $tenant) {
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
if ((int) $tenant->getKey() !== $environmentId) {
continue;
}
@ -537,6 +546,8 @@ private function applyRequestedTenantPrefilter(): void
return;
}
throw new NotFoundHttpException;
}
private function normalizeTenantFilterState(): void
@ -720,9 +731,9 @@ private function incomingGovernanceContext(): ?CanonicalNavigationContext
private function queueUrl(array $overrides = []): string
{
$resolvedTenant = array_key_exists('tenant', $overrides)
? $overrides['tenant']
: $this->filteredTenant()?->external_id;
$resolvedEnvironment = array_key_exists('environment_id', $overrides)
? $overrides['environment_id']
: $this->filteredTenant()?->getKey();
$resolvedView = array_key_exists('view', $overrides)
? $overrides['view']
: $this->currentQueueView();
@ -730,7 +741,7 @@ private function queueUrl(array $overrides = []): string
return static::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => is_string($resolvedTenant) && $resolvedTenant !== '' ? $resolvedTenant : null,
'environment_id' => is_numeric($resolvedEnvironment) ? (int) $resolvedEnvironment : null,
'view' => $resolvedView === 'needs_triage' ? 'needs_triage' : null,
], static fn (mixed $value): bool => $value !== null && $value !== ''),
);

View File

@ -18,6 +18,7 @@
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperateHub\OperateHubShell;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
@ -206,7 +207,7 @@ public function availableFilters(): array
'options' => [],
],
[
'key' => 'tenant',
'key' => 'environment_id',
'label' => 'Managed environment',
'fixed' => false,
'options' => collect($this->visibleTenants())
@ -461,14 +462,22 @@ private function tenantFilterOptions(): array
private function applyRequestedTenantPrefilter(): void
{
$requestedTenant = request()->query('tenant');
$workspace = $this->workspace();
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
if (! $workspace instanceof Workspace) {
return;
}
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
return;
}
$environmentId = $filter->environmentId();
foreach ($this->visibleTenants() as $tenant) {
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
if ((int) $tenant->getKey() !== $environmentId) {
continue;
}
@ -477,6 +486,8 @@ private function applyRequestedTenantPrefilter(): void
return;
}
throw new NotFoundHttpException;
}
private function normalizeTenantFilterState(): void
@ -667,8 +678,8 @@ private function queueUrl(): string
return static::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $tenant?->external_id,
], static fn (mixed $value): bool => $value !== null && $value !== ''),
'environment_id' => $tenant?->getKey(),
], static fn (mixed $value): bool => is_numeric($value)),
);
}

View File

@ -307,8 +307,8 @@ private function assignedFindingsSection(
MyFindingsInbox::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $selectedTenant?->external_id,
], static fn (mixed $value): bool => is_string($value) && $value !== ''),
'environment_id' => $selectedTenant?->getKey(),
], static fn (mixed $value): bool => is_numeric($value)),
),
$navigationContext?->toQuery() ?? [],
),
@ -349,7 +349,7 @@ private function intakeFindingsSection(
FindingsIntakeQueue::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $selectedTenant?->external_id,
'environment_id' => $selectedTenant?->getKey(),
'view' => $needsTriageCount > 0 ? 'needs_triage' : null,
], static fn (mixed $value): bool => $value !== null && $value !== ''),
),

View File

@ -10,6 +10,7 @@
enum AdminSurfaceScope: string
{
case WorkspaceWideSurface = 'workspace_wide_surface';
case WorkspaceOwnedAnalysisSurface = 'workspace_owned_analysis_surface';
case WorkspaceScoped = 'workspace_scoped';
case WorkspaceChooserException = 'workspace_chooser_exception';
case EnvironmentBound = 'environment_bound';
@ -42,6 +43,10 @@ public static function fromPath(string $path): self
return self::WorkspaceWideSurface;
}
if (self::isWorkspaceOwnedAnalysisSurfacePath($normalizedPath)) {
return self::WorkspaceOwnedAnalysisSurface;
}
if (
str_starts_with($normalizedPath, '/admin/evidence/')
&& ! str_starts_with($normalizedPath, '/admin/evidence/overview')
@ -80,6 +85,7 @@ public function allowsEnvironmentlessState(): bool
{
return match ($this) {
self::WorkspaceWideSurface,
self::WorkspaceOwnedAnalysisSurface,
self::WorkspaceScoped,
self::WorkspaceChooserException,
self::OnboardingWorkflow,
@ -92,6 +98,7 @@ public function forcesEnvironmentlessShellContext(): bool
{
return match ($this) {
self::WorkspaceWideSurface,
self::WorkspaceOwnedAnalysisSurface,
self::WorkspaceChooserException,
self::CanonicalWorkspaceRecordViewer => true,
default => false,
@ -116,6 +123,13 @@ private static function isWorkspaceWideSurfacePath(string $normalizedPath): bool
return WorkspaceHubRegistry::isWorkspaceHubPath($normalizedPath);
}
private static function isWorkspaceOwnedAnalysisSurfacePath(string $normalizedPath): bool
{
return preg_match('#^/admin/(baseline-profiles|baseline-snapshots)(?:/.*)?$#', $normalizedPath) === 1
|| preg_match('#^/admin/findings/(?:my-work|intake|hygiene)/?$#', $normalizedPath) === 1
|| preg_match('#^/admin/cross-environment-compare/?$#', $normalizedPath) === 1;
}
private static function effectivePath(Request $request): string
{
$path = '/'.ltrim((string) $request->path(), '/');

View File

@ -21,6 +21,7 @@ public static function fromSurfaceScope(AdminSurfaceScope $pageCategory): self
AdminSurfaceScope::EnvironmentScopedEvidence => self::AdministrativeManagement,
AdminSurfaceScope::CanonicalWorkspaceRecordViewer => self::CanonicalWorkspaceRecord,
AdminSurfaceScope::WorkspaceWideSurface,
AdminSurfaceScope::WorkspaceOwnedAnalysisSurface,
AdminSurfaceScope::WorkspaceScoped,
AdminSurfaceScope::WorkspaceChooserException => self::StandardActiveOperating,
};

View File

@ -18,7 +18,7 @@
</h1>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
Review visible broken assignments and stale in-progress work across entitled tenants in one read-first repair queue. Existing finding detail stays the only place where reassignment or lifecycle repair happens.
Review visible broken assignments and stale in-progress work across entitled environments in one read-first repair queue. Existing finding detail stays the only place where reassignment or lifecycle repair happens.
</p>
</div>
</div>
@ -68,14 +68,11 @@
{{ $scope['reason_filter_label'] }}
</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
ManagedEnvironment prefilter from active context:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
ManagedEnvironment filter applied:
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
Environment filter applied:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@else
All visible tenants are currently included.
All visible environments are currently included.
@endif
</div>
</div>

View File

@ -18,7 +18,7 @@
</h1>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
Review visible unassigned open findings across entitled tenants in one queue. ManagedEnvironment context can narrow the view, but the intake scope stays fixed.
Review visible unassigned open findings across entitled environments in one queue. An explicit environment filter can narrow the view, but the intake scope stays fixed.
</p>
</div>
</div>
@ -68,14 +68,11 @@
{{ $scope['queue_view_label'] }}
</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
ManagedEnvironment prefilter from active context:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
ManagedEnvironment filter applied:
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
Environment filter applied:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@else
All visible tenants are currently included.
All visible environments are currently included.
@endif
</div>
</div>

View File

@ -18,7 +18,7 @@
</h1>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
Review open assigned findings across visible tenants in one queue. ManagedEnvironment context can narrow the view, but the personal assignment scope stays fixed.
Review open assigned findings across visible environments in one queue. An explicit environment filter can narrow the view, but the personal assignment scope stays fixed.
</p>
</div>
</div>
@ -56,14 +56,11 @@
Assigned to me only
</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
ManagedEnvironment prefilter from active context:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
ManagedEnvironment filter applied:
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
Environment filter applied:
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
@else
All visible tenants are currently included.
All visible environments are currently included.
@endif
</div>
</div>

View File

@ -43,11 +43,13 @@ function findingsHygienePage(?User $user = null, array $query = [])
setAdminPanelContext();
$factory = $query === []
? Livewire::actingAs(auth()->user())
: Livewire::withQueryParams($query)->actingAs(auth()->user());
$factory = Livewire::withHeaders(['referer' => FindingsHygieneReport::getUrl(panel: 'admin')]);
return $factory->test(FindingsHygieneReport::class);
if ($query !== []) {
$factory = $factory->withQueryParams($query);
}
return $factory->actingAs(auth()->user())->test(FindingsHygieneReport::class);
}
function makeFindingsHygieneFinding(ManagedEnvironment $tenant, array $attributes = []): Finding
@ -256,7 +258,7 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
'options' => [],
],
[
'key' => 'tenant',
'key' => 'environment_id',
'label' => 'ManagedEnvironment',
'fixed' => false,
'options' => [
@ -351,7 +353,68 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
]);
});
it('explains when the active tenant prefilter hides otherwise visible hygiene issues and clears it in place', function (): void {
it('ignores remembered environments and retired tenant query aliases on the workspace-owned hygiene surface', function (): void {
[$user, $tenantA] = findingsHygieneActingUser();
$tenantB = ManagedEnvironment::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
'name' => 'Beta ManagedEnvironment',
]);
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
$lostMemberA = User::factory()->create(['name' => 'Lost Member A']);
createUserWithTenant($tenantA, $lostMemberA, role: 'readonly', workspaceRole: 'readonly');
ManagedEnvironmentMembership::query()
->where('managed_environment_id', (int) $tenantA->getKey())
->where('user_id', (int) $lostMemberA->getKey())
->delete();
$lostMemberB = User::factory()->create(['name' => 'Lost Member B']);
createUserWithTenant($tenantB, $lostMemberB, role: 'readonly', workspaceRole: 'readonly');
ManagedEnvironmentMembership::query()
->where('managed_environment_id', (int) $tenantB->getKey())
->where('user_id', (int) $lostMemberB->getKey())
->delete();
$tenantAIssue = makeFindingsHygieneFinding($tenantA, [
'owner_user_id' => (int) $user->getKey(),
'assignee_user_id' => (int) $lostMemberA->getKey(),
'subject_display_name' => 'ManagedEnvironment A Issue',
]);
$tenantBIssue = makeFindingsHygieneFinding($tenantB, [
'owner_user_id' => (int) $user->getKey(),
'assignee_user_id' => (int) $lostMemberB->getKey(),
'subject_display_name' => 'ManagedEnvironment B Issue',
]);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = findingsHygienePage($user, [
'tenant' => (string) $tenantB->external_id,
'tenant_id' => (int) $tenantB->getKey(),
'managed_environment_id' => (int) $tenantB->getKey(),
'tenant_scope' => 'environment',
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
],
])
->assertSet('tableFilters.managed_environment_id.value', null)
->assertCanSeeTableRecords([$tenantAIssue, $tenantBIssue]);
expect($component->instance()->appliedScope())->toBe([
'workspace_scoped' => true,
'fixed_scope' => 'visible_findings_hygiene_only',
'reason_filter' => 'all',
'reason_filter_label' => 'All issues',
'tenant_prefilter_source' => 'none',
'tenant_label' => null,
]);
});
it('explains when the explicit environment_id prefilter hides otherwise visible hygiene issues and clears it in place', function (): void {
[$user, $tenantA] = findingsHygieneActingUser();
$tenantB = ManagedEnvironment::factory()->create([
@ -374,11 +437,7 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
'subject_display_name' => 'ManagedEnvironment A Issue',
]);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = findingsHygienePage($user)
$component = findingsHygienePage($user, ['environment_id' => (int) $tenantB->getKey()])
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertCanNotSeeTableRecords([$tenantAIssue])
->assertSee('No hygiene issues match this environment scope')

View File

@ -39,15 +39,16 @@
tenantId: (int) $tenant->getKey(),
familyKey: 'intake_findings',
backLinkUrl: GovernanceInbox::getUrl(panel: 'admin', parameters: [
'managed_environment_id' => (string) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'family' => 'intake_findings',
]),
);
Livewire::withQueryParams(array_replace($context->toQuery(), [
'tenant' => (string) $tenant->external_id,
'view' => 'needs_triage',
]))
Livewire::withHeaders(['referer' => FindingsIntakeQueue::getUrl(panel: 'admin')])
->withQueryParams(array_replace($context->toQuery(), [
'environment_id' => (int) $tenant->getKey(),
'view' => 'needs_triage',
]))
->actingAs($user)
->test(FindingsIntakeQueue::class)
->assertSet('tableFilters.managed_environment_id.value', (string) $tenant->getKey())

View File

@ -31,11 +31,13 @@ function findingsIntakePage(?User $user = null, array $query = [])
setAdminPanelContext();
$factory = $query === []
? Livewire::actingAs(auth()->user())
: Livewire::withQueryParams($query)->actingAs(auth()->user());
$factory = Livewire::withHeaders(['referer' => FindingsIntakeQueue::getUrl(panel: 'admin')]);
return $factory->test(FindingsIntakeQueue::class);
if ($query !== []) {
$factory = $factory->withQueryParams($query);
}
return $factory->actingAs(auth()->user())->test(FindingsIntakeQueue::class);
}
function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []): Finding
@ -140,7 +142,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
->and($queueViews['needs_triage']['active'])->toBeFalse();
});
it('defaults to the active tenant prefilter and lets the operator clear it without dropping intake scope', function (): void {
it('applies the explicit environment_id prefilter and lets the operator clear it without dropping intake scope', function (): void {
[$user, $tenantA] = findingsIntakeActingUser();
$tenantB = ManagedEnvironment::factory()->create([
@ -159,11 +161,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
'status' => Finding::STATUS_TRIAGED,
]);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = findingsIntakePage($user)
$component = findingsIntakePage($user, ['environment_id' => (int) $tenantB->getKey()])
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertCanSeeTableRecords([$findingB])
->assertCanNotSeeTableRecords([$findingA])
@ -174,7 +172,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
'fixed_scope' => 'visible_unassigned_open_findings_only',
'queue_view' => 'unassigned',
'queue_view_label' => 'Unassigned',
'tenant_prefilter_source' => 'active_tenant_context',
'tenant_prefilter_source' => 'explicit_filter',
'tenant_label' => $tenantB->name,
]);
@ -191,7 +189,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
]);
});
it('keeps the needs triage view active when clearing the tenant prefilter', function (): void {
it('keeps the needs triage view active when clearing the environment_id prefilter', function (): void {
[$user, $tenantA] = findingsIntakeActingUser();
$tenantB = ManagedEnvironment::factory()->create([
@ -215,11 +213,10 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
'status' => Finding::STATUS_TRIAGED,
]);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = findingsIntakePage($user, ['view' => 'needs_triage'])
$component = findingsIntakePage($user, [
'environment_id' => (int) $tenantB->getKey(),
'view' => 'needs_triage',
])
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertCanSeeTableRecords([$tenantBTriage])
->assertCanNotSeeTableRecords([$tenantATriage, $tenantBBacklog]);
@ -229,7 +226,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
'fixed_scope' => 'visible_unassigned_open_findings_only',
'queue_view' => 'needs_triage',
'queue_view_label' => 'Needs triage',
'tenant_prefilter_source' => 'active_tenant_context',
'tenant_prefilter_source' => 'explicit_filter',
'tenant_label' => $tenantB->name,
]);
@ -252,6 +249,51 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
->and($queueViews['needs_triage']['active'])->toBeTrue();
});
it('ignores remembered environments and retired tenant query aliases on the workspace-owned intake surface', function (): void {
[$user, $tenantA] = findingsIntakeActingUser();
$tenantB = ManagedEnvironment::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
'name' => 'Beta ManagedEnvironment',
]);
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
$findingA = makeIntakeFinding($tenantA, [
'subject_external_id' => 'tenant-a',
'status' => Finding::STATUS_NEW,
]);
$findingB = makeIntakeFinding($tenantB, [
'subject_external_id' => 'tenant-b',
'status' => Finding::STATUS_TRIAGED,
]);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = findingsIntakePage($user, [
'tenant' => (string) $tenantB->external_id,
'tenant_id' => (int) $tenantB->getKey(),
'managed_environment_id' => (int) $tenantB->getKey(),
'tenant_scope' => 'environment',
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
],
])
->assertSet('tableFilters.managed_environment_id.value', null)
->assertCanSeeTableRecords([$findingA, $findingB]);
expect($component->instance()->appliedScope())->toBe([
'workspace_scoped' => true,
'fixed_scope' => 'visible_unassigned_open_findings_only',
'queue_view' => 'unassigned',
'queue_view_label' => 'Unassigned',
'tenant_prefilter_source' => 'none',
'tenant_label' => null,
]);
});
it('separates needs triage from the remaining backlog and keeps deterministic urgency ordering', function (): void {
[$user, $tenant] = findingsIntakeActingUser();
@ -303,7 +345,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
]);
$component = findingsIntakePage($user, [
'tenant' => (string) $tenant->external_id,
'environment_id' => (int) $tenant->getKey(),
'view' => 'needs_triage',
]);
@ -333,9 +375,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
'subject_external_id' => 'available-elsewhere',
]);
findingsIntakePage($user, [
'tenant' => (string) $tenantA->external_id,
])
findingsIntakePage($user, ['environment_id' => (int) $tenantA->getKey()])
->assertSee('No intake findings match this environment scope')
->assertTableEmptyStateActionsExistInOrder(['clear_tenant_filter_empty']);

View File

@ -38,14 +38,15 @@
tenantId: (int) $tenant->getKey(),
familyKey: 'assigned_findings',
backLinkUrl: GovernanceInbox::getUrl(panel: 'admin', parameters: [
'managed_environment_id' => (string) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'family' => 'assigned_findings',
]),
);
Livewire::withQueryParams(array_replace($context->toQuery(), [
'tenant' => (string) $tenant->external_id,
]))
Livewire::withHeaders(['referer' => MyFindingsInbox::getUrl(panel: 'admin')])
->withQueryParams(array_replace($context->toQuery(), [
'environment_id' => (int) $tenant->getKey(),
]))
->actingAs($user)
->test(MyFindingsInbox::class)
->assertSet('tableFilters.managed_environment_id.value', (string) $tenant->getKey())

View File

@ -31,9 +31,13 @@ function myWorkInboxPage(?User $user = null, array $query = [])
setAdminPanelContext();
$factory = $query === [] ? Livewire::actingAs(auth()->user()) : Livewire::withQueryParams($query)->actingAs(auth()->user());
$factory = Livewire::withHeaders(['referer' => MyFindingsInbox::getUrl(panel: 'admin')]);
return $factory->test(MyFindingsInbox::class);
if ($query !== []) {
$factory = $factory->withQueryParams($query);
}
return $factory->actingAs(auth()->user())->test(MyFindingsInbox::class);
}
function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee, array $attributes = []): Finding
@ -121,7 +125,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
'options' => [],
],
[
'key' => 'tenant',
'key' => 'environment_id',
'label' => 'Managed environment',
'fixed' => false,
'options' => [
@ -150,7 +154,50 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
]);
});
it('defaults to the active environment prefilter and lets the operator clear it without dropping personal scope', function (): void {
it('applies the explicit environment_id prefilter and lets the operator clear it without dropping personal scope', function (): void {
[$user, $tenantA] = myWorkInboxActingUser();
$tenantB = ManagedEnvironment::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
'name' => 'Beta ManagedEnvironment',
]);
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
$findingA = makeAssignedFindingForInbox($tenantA, $user, [
'subject_external_id' => 'tenant-a',
'status' => Finding::STATUS_NEW,
]);
$findingB = makeAssignedFindingForInbox($tenantB, $user, [
'subject_external_id' => 'tenant-b',
'status' => Finding::STATUS_TRIAGED,
]);
$component = myWorkInboxPage($user, ['environment_id' => (int) $tenantB->getKey()])
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertCanSeeTableRecords([$findingB])
->assertCanNotSeeTableRecords([$findingA])
->assertActionVisible('clear_tenant_filter');
expect($component->instance()->appliedScope())->toBe([
'workspace_scoped' => true,
'assignee_scope' => 'current_user_only',
'tenant_prefilter_source' => 'explicit_filter',
'tenant_label' => $tenantB->name,
]);
$component->callAction('clear_tenant_filter')
->assertCanSeeTableRecords([$findingA, $findingB]);
expect($component->instance()->appliedScope())->toBe([
'workspace_scoped' => true,
'assignee_scope' => 'current_user_only',
'tenant_prefilter_source' => 'none',
'tenant_label' => null,
]);
});
it('ignores remembered environments and retired tenant query aliases on the workspace-owned analysis surface', function (): void {
[$user, $tenantA] = myWorkInboxActingUser();
$tenantB = ManagedEnvironment::factory()->create([
@ -173,20 +220,16 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
]);
$component = myWorkInboxPage($user)
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertCanSeeTableRecords([$findingB])
->assertCanNotSeeTableRecords([$findingA])
->assertActionVisible('clear_tenant_filter');
expect($component->instance()->appliedScope())->toBe([
'workspace_scoped' => true,
'assignee_scope' => 'current_user_only',
'tenant_prefilter_source' => 'active_tenant_context',
'tenant_label' => $tenantB->name,
]);
$component->callAction('clear_tenant_filter')
$component = myWorkInboxPage($user, [
'tenant' => (string) $tenantB->external_id,
'tenant_id' => (int) $tenantB->getKey(),
'managed_environment_id' => (int) $tenantB->getKey(),
'tenant_scope' => 'environment',
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
],
])
->assertSet('tableFilters.managed_environment_id.value', null)
->assertCanSeeTableRecords([$findingA, $findingB]);
expect($component->instance()->appliedScope())->toBe([
@ -282,9 +325,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
'subject_external_id' => 'available-elsewhere',
]);
$component = myWorkInboxPage($user, [
'tenant' => (string) $tenantA->external_id,
])
$component = myWorkInboxPage($user, ['environment_id' => (int) $tenantA->getKey()])
->assertCanNotSeeTableRecords([])
->assertSee('No assigned findings match this environment scope')
->assertTableEmptyStateActionsExistInOrder(['clear_tenant_filter_empty']);
@ -306,7 +347,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
->assertTableEmptyStateActionsExistInOrder(['choose_environment_empty']);
});
it('uses the active visible environment for the calm empty-state drillback when environment context exists', function (): void {
it('keeps the calm empty-state drillback workspace-owned when remembered environment context exists', function (): void {
[$user, $tenant] = myWorkInboxActingUser();
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
@ -315,13 +356,13 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
$component = myWorkInboxPage($user)
->assertSee('No visible assigned findings right now')
->assertTableEmptyStateActionsExistInOrder(['open_tenant_findings_empty']);
->assertTableEmptyStateActionsExistInOrder(['choose_environment_empty']);
expect($component->instance()->emptyState())->toMatchArray([
'action_name' => 'open_tenant_findings_empty',
'action_label' => 'Open environment findings',
'action_name' => 'choose_environment_empty',
'action_label' => 'Choose an environment',
'action_kind' => 'url',
'action_url' => FindingResource::getUrl('index', panel: 'admin', tenant: $tenant),
'action_url' => route('filament.admin.pages.choose-environment'),
]);
});

View File

@ -44,6 +44,13 @@
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeFalse()
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeTrue()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-compare-landing'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-profiles'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-profiles/42/compare-matrix'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-snapshots'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/my-work'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/intake'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/hygiene'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/cross-environment-compare'))->toBeFalse()
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeFalse()
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeTrue();
});

View File

@ -83,6 +83,81 @@
->and($resolved->recoveryReason)->toBeNull();
});
it('keeps workspace owned analysis surfaces tenantless when a remembered environment exists', function (string $path): void {
$rememberedEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Remembered ManagedEnvironment']);
[$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner');
$this->actingAs($user);
Filament::setTenant(null, true);
$workspaceId = (int) $rememberedEnvironment->workspace_id;
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
]);
$request = Request::create($path);
$request->setLaravelSession(app('session.store'));
$request->setUserResolver(static fn () => $user);
$resolved = app(OperateHubShell::class)->resolvedContext($request);
expect($resolved->workspace?->getKey())->toBe($workspaceId)
->and($resolved->tenant)->toBeNull()
->and($resolved->tenantSource)->toBe('none')
->and($resolved->state)->toBe('tenantless_workspace');
})->with([
'baseline profiles list' => ['/admin/baseline-profiles'],
'baseline profiles detail' => ['/admin/baseline-profiles/42'],
'baseline profiles edit' => ['/admin/baseline-profiles/42/edit'],
'baseline profiles compare matrix' => ['/admin/baseline-profiles/42/compare-matrix'],
'baseline snapshots list' => ['/admin/baseline-snapshots'],
'baseline snapshots detail' => ['/admin/baseline-snapshots/42'],
'my findings' => ['/admin/findings/my-work'],
'findings intake' => ['/admin/findings/intake'],
'findings hygiene' => ['/admin/findings/hygiene'],
'cross-environment compare' => ['/admin/cross-environment-compare'],
]);
it('does not resolve explicit environment_id query hints as shell tenant context on workspace owned analysis surfaces', function (string $path): void {
$workspaceTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Workspace ManagedEnvironment']);
[$user, $workspaceTenant] = createUserWithTenant(tenant: $workspaceTenant, role: 'owner');
$hintedTenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspaceTenant->workspace_id,
'name' => 'Hinted ManagedEnvironment',
]);
createUserWithTenant(tenant: $hintedTenant, user: $user, role: 'owner');
$this->actingAs($user);
Filament::setTenant(null, true);
$workspaceId = (int) $workspaceTenant->workspace_id;
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
$request = Request::create($path, parameters: [
'environment_id' => (int) $hintedTenant->getKey(),
]);
$request->setLaravelSession(app('session.store'));
$request->setUserResolver(static fn () => $user);
$resolved = app(OperateHubShell::class)->resolvedContext($request);
expect($resolved->workspace?->getKey())->toBe($workspaceId)
->and($resolved->tenant)->toBeNull()
->and($resolved->tenantSource)->toBe('none')
->and($resolved->state)->toBe('tenantless_workspace');
})->with([
'baseline profiles' => ['/admin/baseline-profiles'],
'baseline snapshots' => ['/admin/baseline-snapshots'],
'my findings' => ['/admin/findings/my-work'],
'findings intake' => ['/admin/findings/intake'],
'findings hygiene' => ['/admin/findings/hygiene'],
'cross-environment compare' => ['/admin/cross-environment-compare'],
]);
it('uses the routed tenant workspace when the tenant panel is entered without a selected workspace session', function (): void {
$tenant = ManagedEnvironment::factory()->active()->create(['name' => 'ManagedEnvironment Panel Scope']);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');

View File

@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Support\Navigation\AdminSurfaceScope;
use App\Support\Tenants\TenantInteractionLane;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
@ -17,6 +18,16 @@
'retired tenant panel route' => ['/admin/t/tenant-123', AdminSurfaceScope::WorkspaceScoped],
'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', AdminSurfaceScope::EnvironmentBound],
'baseline compare environment route' => ['/admin/workspaces/acme/environments/tenant-123/baseline-compare', AdminSurfaceScope::EnvironmentBound],
'baseline profiles list' => ['/admin/baseline-profiles', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'baseline profiles detail' => ['/admin/baseline-profiles/42', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'baseline profiles edit' => ['/admin/baseline-profiles/42/edit', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'baseline profiles compare matrix' => ['/admin/baseline-profiles/42/compare-matrix', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'baseline snapshots list' => ['/admin/baseline-snapshots', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'baseline snapshots detail' => ['/admin/baseline-snapshots/42', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'my findings inbox' => ['/admin/findings/my-work', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'findings intake' => ['/admin/findings/intake', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'findings hygiene' => ['/admin/findings/hygiene', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'cross-environment compare' => ['/admin/cross-environment-compare', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
'tenant scoped evidence detail' => ['/admin/evidence/123', AdminSurfaceScope::EnvironmentScopedEvidence],
'evidence overview' => ['/admin/evidence/overview', AdminSurfaceScope::WorkspaceWideSurface],
'customer review workspace' => ['/admin/reviews/workspace', AdminSurfaceScope::WorkspaceWideSurface],
@ -31,3 +42,14 @@
'retired operation run detail' => ['/admin/operations/44', AdminSurfaceScope::WorkspaceScoped],
'operation run detail' => ['/admin/workspaces/acme/operations/44', AdminSurfaceScope::CanonicalWorkspaceRecordViewer],
]);
it('keeps workspace owned analysis surfaces tenantless without query hint or remembered environment restore', function (): void {
$surface = AdminSurfaceScope::WorkspaceOwnedAnalysisSurface;
expect($surface->allowsQueryEnvironmentHints())->toBeFalse()
->and($surface->allowsRememberedEnvironmentRestore())->toBeFalse()
->and($surface->allowsEnvironmentlessState())->toBeTrue()
->and($surface->forcesEnvironmentlessShellContext())->toBeTrue()
->and($surface->requiresExplicitEnvironment())->toBeFalse()
->and($surface->lane())->toBe(TenantInteractionLane::StandardActiveOperating);
});