From ef9380ac32daf75f7e7413de914cb596bef4792c Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 17 May 2026 00:22:17 +0200 Subject: [PATCH] feat: cut over workspace-owned analysis shell context --- .../Pages/Findings/FindingsHygieneReport.php | 27 ++++-- .../Pages/Findings/FindingsIntakeQueue.php | 27 ++++-- .../Pages/Findings/MyFindingsInbox.php | 23 +++-- .../GovernanceInboxSectionBuilder.php | 6 +- .../Support/Navigation/AdminSurfaceScope.php | 14 +++ .../Support/Tenants/TenantInteractionLane.php | 1 + .../findings-hygiene-report.blade.php | 11 +-- .../findings/findings-intake-queue.blade.php | 11 +-- .../findings/my-findings-inbox.blade.php | 11 +-- .../FindingsAssignmentHygieneReportTest.php | 81 +++++++++++++--- ...ndingsIntakeQueueNavigationContextTest.php | 11 ++- .../Findings/FindingsIntakeQueueTest.php | 84 ++++++++++++----- .../MyFindingsInboxNavigationContextTest.php | 9 +- .../Feature/Findings/MyWorkInboxTest.php | 93 +++++++++++++------ .../Navigation/WorkspaceHubRegistryTest.php | 7 ++ .../OperateHubShellResolutionTest.php | 75 +++++++++++++++ .../Unit/Tenants/AdminSurfaceScopeTest.php | 22 +++++ 17 files changed, 399 insertions(+), 114 deletions(-) diff --git a/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php b/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php index b658781a..17b3bb59 100644 --- a/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php +++ b/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php @@ -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, diff --git a/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php b/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php index f70cf920..29689202 100644 --- a/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php +++ b/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php @@ -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 !== ''), ); diff --git a/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php b/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php index 962b97be..be4ba8b3 100644 --- a/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php +++ b/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php @@ -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)), ); } diff --git a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php index f413c818..7e056e64 100644 --- a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php +++ b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php @@ -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 !== ''), ), diff --git a/apps/platform/app/Support/Navigation/AdminSurfaceScope.php b/apps/platform/app/Support/Navigation/AdminSurfaceScope.php index cdb599a8..5dcf0b88 100644 --- a/apps/platform/app/Support/Navigation/AdminSurfaceScope.php +++ b/apps/platform/app/Support/Navigation/AdminSurfaceScope.php @@ -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(), '/'); diff --git a/apps/platform/app/Support/Tenants/TenantInteractionLane.php b/apps/platform/app/Support/Tenants/TenantInteractionLane.php index 7103d79a..689ed9e5 100644 --- a/apps/platform/app/Support/Tenants/TenantInteractionLane.php +++ b/apps/platform/app/Support/Tenants/TenantInteractionLane.php @@ -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, }; diff --git a/apps/platform/resources/views/filament/pages/findings/findings-hygiene-report.blade.php b/apps/platform/resources/views/filament/pages/findings/findings-hygiene-report.blade.php index 51857d66..88c2d3d4 100644 --- a/apps/platform/resources/views/filament/pages/findings/findings-hygiene-report.blade.php +++ b/apps/platform/resources/views/filament/pages/findings/findings-hygiene-report.blade.php @@ -18,7 +18,7 @@

- 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.

@@ -68,14 +68,11 @@ {{ $scope['reason_filter_label'] }}
- @if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context') - ManagedEnvironment prefilter from active context: - {{ $scope['tenant_label'] }} - @elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') - ManagedEnvironment filter applied: + @if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') + Environment filter applied: {{ $scope['tenant_label'] }} @else - All visible tenants are currently included. + All visible environments are currently included. @endif
diff --git a/apps/platform/resources/views/filament/pages/findings/findings-intake-queue.blade.php b/apps/platform/resources/views/filament/pages/findings/findings-intake-queue.blade.php index 623703bc..f535c195 100644 --- a/apps/platform/resources/views/filament/pages/findings/findings-intake-queue.blade.php +++ b/apps/platform/resources/views/filament/pages/findings/findings-intake-queue.blade.php @@ -18,7 +18,7 @@

- 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.

@@ -68,14 +68,11 @@ {{ $scope['queue_view_label'] }}
- @if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context') - ManagedEnvironment prefilter from active context: - {{ $scope['tenant_label'] }} - @elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') - ManagedEnvironment filter applied: + @if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') + Environment filter applied: {{ $scope['tenant_label'] }} @else - All visible tenants are currently included. + All visible environments are currently included. @endif
diff --git a/apps/platform/resources/views/filament/pages/findings/my-findings-inbox.blade.php b/apps/platform/resources/views/filament/pages/findings/my-findings-inbox.blade.php index d4a4e4b3..290f135d 100644 --- a/apps/platform/resources/views/filament/pages/findings/my-findings-inbox.blade.php +++ b/apps/platform/resources/views/filament/pages/findings/my-findings-inbox.blade.php @@ -18,7 +18,7 @@

- 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.

@@ -56,14 +56,11 @@ Assigned to me only
- @if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context') - ManagedEnvironment prefilter from active context: - {{ $scope['tenant_label'] }} - @elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') - ManagedEnvironment filter applied: + @if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter') + Environment filter applied: {{ $scope['tenant_label'] }} @else - All visible tenants are currently included. + All visible environments are currently included. @endif
diff --git a/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php b/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php index 3fd8fa24..260df0e8 100644 --- a/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php +++ b/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php @@ -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') diff --git a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueNavigationContextTest.php b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueNavigationContextTest.php index f3f2bcb8..4b1c1069 100644 --- a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueNavigationContextTest.php +++ b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueNavigationContextTest.php @@ -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()) diff --git a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php index 9d58a6fb..1d8fb9ba 100644 --- a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php +++ b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php @@ -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']); diff --git a/apps/platform/tests/Feature/Findings/MyFindingsInboxNavigationContextTest.php b/apps/platform/tests/Feature/Findings/MyFindingsInboxNavigationContextTest.php index 5bdd826b..c39a91a1 100644 --- a/apps/platform/tests/Feature/Findings/MyFindingsInboxNavigationContextTest.php +++ b/apps/platform/tests/Feature/Findings/MyFindingsInboxNavigationContextTest.php @@ -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()) diff --git a/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php b/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php index 4f5b497e..2534a8e4 100644 --- a/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php +++ b/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php @@ -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'), ]); }); diff --git a/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php b/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php index c335d1cb..4ab22f28 100644 --- a/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php +++ b/apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php @@ -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(); }); diff --git a/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php b/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php index 58112f10..7f107a18 100644 --- a/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php +++ b/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php @@ -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'); diff --git a/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php index 4d6bb610..a536abb5 100644 --- a/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php +++ b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php @@ -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); +});