test: add Spec 322 browser no-drift regression guards
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 52s

This commit is contained in:
Ahmed Darrazi 2026-05-17 12:57:29 +02:00
parent d879c61204
commit d5086ff35a
15 changed files with 2378 additions and 0 deletions

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDestinationResource;
use App\Filament\Resources\AlertRuleResource;
use Tests\Browser\Support\Spec322WorkspaceEnvironmentBrowserHarness as Spec322Harness;
pest()->browser()->timeout(45_000);
it('Spec322 smokes alerts overview and alert deliveries no drift behavior', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
Spec322Harness::assertWorkspaceOnly(
visit(route('filament.admin.alerts'))->waitForText('Alerts'),
null,
$fixture['environmentA']->name,
);
$filteredAlerts = visit(route('filament.admin.alerts', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
]));
Spec322Harness::assertFilteredWorkspaceHub($filteredAlerts, $fixture['environmentA'], 'Sent');
$page = visit(AlertDeliveryResource::getUrl('index', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
], panel: 'admin'));
Spec322Harness::assertFilteredWorkspaceHub($page, $fixture['environmentA'], 'Sent');
Spec322Harness::clearWorkspaceHubEnvironmentFilter($page);
Spec322Harness::assertWorkspaceOnly($page, 'Sent', $fixture['environmentA']->name);
$page->script('window.location.reload();');
Spec322Harness::assertWorkspaceOnly($page, 'Sent', $fixture['environmentA']->name);
$page->script('window.history.back();');
Spec322Harness::assertFilteredWorkspaceHub($page, $fixture['environmentA'], 'Sent');
$page->script('window.history.forward();');
Spec322Harness::assertWorkspaceOnly($page, 'Sent', $fixture['environmentA']->name);
});
it('Spec322 smokes audit log filtered and clean entries without shell drift', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
Spec322Harness::assertFilteredWorkspaceHub(
visit(route('admin.monitoring.audit-log', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
])),
$fixture['environmentA'],
'Spec322 browser audit B',
);
$page = visit(route('admin.monitoring.audit-log'));
Spec322Harness::assertWorkspaceOnly($page, 'Spec322 browser audit B', $fixture['environmentA']->name);
$page->script('window.location.reload();');
Spec322Harness::assertWorkspaceOnly($page, 'Spec322 browser audit B', $fixture['environmentA']->name);
});
it('Spec322 smokes alert configuration surfaces ignore stray environment filters', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
$configurationUrls = [
AlertRuleResource::getUrl('index', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
], panel: 'admin'),
AlertDestinationResource::getUrl('index', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
], panel: 'admin'),
];
foreach ($configurationUrls as $url) {
visit($url)
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
}
});

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\EnvironmentDashboard;
use App\Support\ManagedEnvironmentLinks;
use Tests\Browser\Support\Spec322WorkspaceEnvironmentBrowserHarness as Spec322Harness;
pest()->browser()->timeout(45_000);
it('Spec322 smokes environment owned route and shell contracts', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
$environmentRoutes = [
'environment dashboard' => [
'url' => EnvironmentDashboard::getUrl(panel: 'admin', tenant: $fixture['environmentA']),
'text' => $fixture['environmentA']->name,
],
'baseline compare' => [
'url' => ManagedEnvironmentLinks::baselineCompareUrl($fixture['environmentA']),
'text' => 'Baseline Compare',
],
'required permissions' => [
'url' => ManagedEnvironmentLinks::requiredPermissionsUrl($fixture['environmentA']),
'text' => 'Required permissions',
],
];
foreach ($environmentRoutes as $route) {
$page = visit($route['url'])
->waitForText($route['text'])
->assertSee($fixture['environmentA']->name)
->assertScript('window.location.pathname.includes("/workspaces/")', true)
->assertScript('window.location.pathname.includes("/environments/")', true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page->script('window.location.reload();');
$page
->waitForText($route['text'])
->assertSee($fixture['environmentA']->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
}
});
it('Spec322 smokes baseline compare rejects old workspace style access', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
visit('/admin/baseline-compare-landing?environment_id='.(int) $fixture['environmentA']->getKey())
->assertScript(
'document.body.innerText.includes("404") || document.body.innerText.includes("Not Found") || document.body.innerText.includes("No access")',
true,
)
->assertNoJavaScriptErrors();
});

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\EnvironmentDashboard;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Resources\ProviderConnectionResource;
use App\Support\OperationRunLinks;
use Tests\Browser\Support\Spec322WorkspaceEnvironmentBrowserHarness as Spec322Harness;
pest()->browser()->timeout(90_000);
it('Spec322 smokes clean workspace hub entry from environment origin without drift', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
visit(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $fixture['environmentA']))
->assertSee($fixture['environmentA']->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$cleanHubs = [
'operations' => [
'url' => OperationRunLinks::index(workspace: $fixture['workspace']),
'wide_text' => 'Inventory sync',
],
'governance inbox' => [
'url' => GovernanceInbox::getUrl(panel: 'admin'),
'wide_text' => 'Spec322 Browser Governance B',
],
'provider connections' => [
'url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
'wide_text' => 'Spec322 Browser Provider B',
],
'evidence overview' => [
'url' => route('admin.evidence.overview'),
'wide_text' => $fixture['environmentB']->name,
],
];
foreach ($cleanHubs as $hub) {
$page = visit($hub['url']);
Spec322Harness::assertWorkspaceOnly($page, $hub['wide_text'], $fixture['environmentA']->name);
$page->script('window.location.reload();');
Spec322Harness::assertWorkspaceOnly($page, $hub['wide_text'], $fixture['environmentA']->name);
}
});
it('Spec322 smokes filtered workspace hub clear reload and history alignment', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
$filteredHubs = [
'provider connections' => [
'filtered_url' => ProviderConnectionResource::getUrl('index', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
], panel: 'admin'),
'wide_text' => 'Spec322 Browser Provider B',
'hidden_text' => 'Spec322 Browser Provider B',
],
'evidence overview' => [
'filtered_url' => route('admin.evidence.overview', [
'environment_id' => (int) $fixture['environmentA']->getKey(),
]),
'wide_text' => $fixture['environmentB']->name,
'hidden_text' => $fixture['environmentB']->name,
],
];
foreach ($filteredHubs as $hub) {
$page = visit($hub['filtered_url']);
Spec322Harness::assertFilteredWorkspaceHub($page, $fixture['environmentA'], $hub['hidden_text']);
Spec322Harness::clearWorkspaceHubEnvironmentFilter($page);
Spec322Harness::assertWorkspaceOnly($page, $hub['wide_text'], $fixture['environmentA']->name);
$page->script('window.location.reload();');
Spec322Harness::assertWorkspaceOnly($page, $hub['wide_text'], $fixture['environmentA']->name);
$page->script('window.history.back();');
Spec322Harness::assertFilteredWorkspaceHub($page, $fixture['environmentA'], $hub['hidden_text']);
$page->script('window.history.forward();');
Spec322Harness::assertWorkspaceOnly($page, $hub['wide_text'], $fixture['environmentA']->name);
}
});
it('Spec322 smokes representative legacy aliases without creating filter state', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
$legacyUrls = [
ProviderConnectionResource::getUrl('index', [
'managed_environment_id' => (int) $fixture['environmentA']->getKey(),
], panel: 'admin'),
FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
'tenant' => (string) $fixture['environmentA']->getKey(),
]),
route('admin.monitoring.audit-log', [
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $fixture['environmentA']->getKey()],
],
]),
];
foreach ($legacyUrls as $url) {
visit($url)
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($fixture['environmentB']->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
}
});

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\CrossEnvironmentComparePage;
use App\Filament\Pages\EnvironmentDashboard;
use App\Filament\Pages\Findings\MyFindingsInbox;
use App\Filament\Pages\Settings\WorkspaceSettings;
use App\Filament\Resources\BaselineProfileResource;
use Tests\Browser\Support\Spec322WorkspaceEnvironmentBrowserHarness as Spec322Harness;
pest()->browser()->timeout(45_000);
it('Spec322 smokes workspace owned analysis and configuration surfaces stay workspace only', function (): void {
$fixture = Spec322Harness::fixture();
Spec322Harness::authenticate($this, $fixture['user'], $fixture['workspace'], $fixture['environmentA']);
visit(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $fixture['environmentA']))
->assertSee($fixture['environmentA']->name)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$surfaces = [
'baseline profiles' => [
'url' => BaselineProfileResource::getUrl('index', panel: 'admin'),
],
'my findings' => [
'url' => MyFindingsInbox::getUrl(panel: 'admin'),
],
'cross-environment compare' => [
'url' => CrossEnvironmentComparePage::getUrl(panel: 'admin'),
],
'workspace settings' => [
'url' => WorkspaceSettings::getUrl(panel: 'admin'),
],
];
foreach ($surfaces as $surface) {
$expectedPath = json_encode((string) parse_url($surface['url'], PHP_URL_PATH), JSON_THROW_ON_ERROR);
$page = visit($surface['url']);
Spec322Harness::assertWorkspaceOnly($page, null, $fixture['environmentA']->name);
$page->assertScript("window.location.pathname === {$expectedPath}", true);
$page->script('window.location.reload();');
Spec322Harness::assertWorkspaceOnly($page, null, $fixture['environmentA']->name);
$page->assertScript("window.location.pathname === {$expectedPath}", true);
}
});

View File

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace Tests\Browser\Support;
use App\Models\AlertDelivery;
use App\Models\AlertDestination;
use App\Models\AlertRule;
use App\Models\AuditLog;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Models\Workspace;
use App\Support\EnvironmentReviewStatus;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Workspaces\WorkspaceContext;
final class Spec322WorkspaceEnvironmentBrowserHarness
{
/**
* @return array{
* user: User,
* workspace: Workspace,
* environmentA: ManagedEnvironment,
* environmentB: ManagedEnvironment,
* runA: OperationRun,
* runB: OperationRun,
* connectionA: ProviderConnection,
* connectionB: ProviderConnection,
* exceptionA: FindingException,
* exceptionB: FindingException,
* snapshotA: EvidenceSnapshot,
* snapshotB: EvidenceSnapshot,
* reviewA: EnvironmentReview,
* reviewB: EnvironmentReview,
* deliveryA: AlertDelivery,
* deliveryB: AlertDelivery,
* auditA: AuditLog,
* auditB: AuditLog,
* auditWorkspace: AuditLog
* }
*/
public static function fixture(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec322 Browser Environment A',
'external_id' => 'spec322-browser-environment-a',
]);
[$user, $environmentA] = \createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec322 Browser Environment B',
'external_id' => 'spec322-browser-environment-b',
]);
\createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
$workspace = $environmentA->workspace()->firstOrFail();
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
$exceptionA = self::findingException($environmentA, $user, 'Spec322 Browser Governance A', 'Spec322 Browser Decision A');
$exceptionB = self::findingException($environmentB, $user, 'Spec322 Browser Governance B', 'Spec322 Browser Decision B');
$connectionA = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $environmentA->getKey(),
'display_name' => 'Spec322 Browser Provider A',
]);
$connectionB = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $environmentB->getKey(),
'display_name' => 'Spec322 Browser Provider B',
]);
$snapshotA = self::evidenceSnapshot($environmentA);
$snapshotB = self::evidenceSnapshot($environmentB);
$reviewA = self::publishedReview($environmentA, $user, $snapshotA);
$reviewB = self::publishedReview($environmentB, $user, $snapshotB);
$rule = AlertRule::factory()->create(['workspace_id' => (int) $workspace->getKey()]);
$destination = AlertDestination::factory()->create(['workspace_id' => (int) $workspace->getKey()]);
$deliveryA = AlertDelivery::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $environmentA->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_FAILED,
'created_at' => now()->subHour(),
]);
$deliveryB = AlertDelivery::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $environmentB->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_SENT,
'created_at' => now()->subHour(),
]);
$auditA = self::auditRecord($environmentA, 'Spec322 browser audit A');
$auditB = self::auditRecord($environmentB, 'Spec322 browser audit B');
$auditWorkspace = self::auditRecord(null, 'Spec322 browser workspace audit', [
'workspace_id' => (int) $workspace->getKey(),
]);
return compact(
'user',
'workspace',
'environmentA',
'environmentB',
'runA',
'runB',
'connectionA',
'connectionB',
'exceptionA',
'exceptionB',
'snapshotA',
'snapshotB',
'reviewA',
'reviewB',
'deliveryA',
'deliveryB',
'auditA',
'auditB',
'auditWorkspace',
);
}
public static function authenticate(object $testCase, User $user, Workspace $workspace, ?ManagedEnvironment $rememberedEnvironment = null): void
{
$session = [
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
];
if ($rememberedEnvironment instanceof ManagedEnvironment) {
$session[WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY] = [
(string) $workspace->getKey() => (int) $rememberedEnvironment->getKey(),
];
}
$testCase->actingAs($user)->withSession($session);
foreach ($session as $key => $value) {
session()->put($key, $value);
}
\setAdminPanelContext($rememberedEnvironment);
}
public static function assertWorkspaceOnly(mixed $page, ?string $wideText = null, ?string $environmentName = null): mixed
{
$page
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
if ($wideText !== null) {
$page->assertSee($wideText);
}
if ($environmentName !== null) {
$page->assertDontSee(__('localization.shell.environment_scope').': '.$environmentName);
}
return self::assertNoLegacyQuery($page);
}
public static function assertFilteredWorkspaceHub(mixed $page, ManagedEnvironment $environment, ?string $hiddenText = null): mixed
{
$page
->waitForText('Environment filter:')
->assertSee($environment->name)
->assertDontSee(__('localization.shell.environment_scope').': '.$environment->name)
->assertSee('Clear filter')
->assertScript('window.location.search.includes("environment_id=")', true)
->assertScript('! window.location.search.includes("tenant=")', true)
->assertScript('! window.location.search.includes("tenant_id=")', true)
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
->assertScript('! window.location.search.includes("tenant_scope=")', true)
->assertScript('! window.location.search.includes("tableFilters")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
if ($hiddenText !== null) {
$page->assertDontSee($hiddenText);
}
return $page;
}
public static function assertNoLegacyQuery(mixed $page): mixed
{
return $page
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertScript('! window.location.search.includes("tenant=")', true)
->assertScript('! window.location.search.includes("tenant_id=")', true)
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
->assertScript('! window.location.search.includes("tenant_scope=")', true)
->assertScript('! window.location.search.includes("tableFilters")', true);
}
public static function clearWorkspaceHubEnvironmentFilter(mixed $page): mixed
{
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
return $page->waitForText(__('localization.shell.no_environment_selected'));
}
private static function findingException(
ManagedEnvironment $environment,
User $actor,
string $requestReason,
string $decisionReason,
): FindingException {
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
'workspace_id' => (int) $environment->workspace_id,
'subject_external_id' => str()->slug($requestReason),
]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $actor->getKey(),
'owner_user_id' => (int) $actor->getKey(),
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => $requestReason,
'requested_at' => now()->subDay(),
'review_due_at' => now()->addDay(),
'evidence_summary' => ['reference_count' => 0],
]);
$decision = $exception->decisions()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'actor_user_id' => (int) $actor->getKey(),
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
'reason' => $decisionReason,
'metadata' => [],
'decided_at' => now()->subDay(),
]);
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
return $exception->fresh(['currentDecision']);
}
private static function evidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
{
return EvidenceSnapshot::query()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'status' => EvidenceSnapshotStatus::Active->value,
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
'generated_at' => now(),
]);
}
private static function publishedReview(ManagedEnvironment $environment, User $user, EvidenceSnapshot $snapshot): EnvironmentReview
{
$review = \composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
return $review->fresh();
}
/**
* @param array<string, mixed> $attributes
*/
private static function auditRecord(?ManagedEnvironment $environment, string $summary, array $attributes = []): AuditLog
{
$workspaceId = array_key_exists('workspace_id', $attributes)
? (int) $attributes['workspace_id']
: (int) ($environment?->workspace_id);
return AuditLog::query()->create(array_merge([
'workspace_id' => $workspaceId,
'managed_environment_id' => $environment?->getKey(),
'actor_email' => 'spec322-browser@example.test',
'actor_name' => 'Spec322 Browser Operator',
'action' => 'operation.completed',
'status' => 'success',
'resource_type' => 'operation_run',
'resource_id' => '322',
'summary' => $summary,
'metadata' => [],
'recorded_at' => now(),
], $attributes));
}
}

View File

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\ProviderConnectionResource;
use App\Models\ManagedEnvironment;
use App\Support\Navigation\AdminSurfaceScope;
use App\Support\Navigation\WorkspaceHubRegistry;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
it('classifies_core_admin_surfaces_without_scope_drift', function (): void {
$workspaceHubPaths = [
'/admin',
'/admin/workspaces/acme/overview',
'/admin/workspaces/acme/operations',
'/admin/provider-connections',
'/admin/finding-exceptions/queue',
'/admin/evidence/overview',
'/admin/reviews',
'/admin/reviews/workspace',
'/admin/governance/inbox',
'/admin/governance/decisions',
'/admin/audit-log',
'/admin/alerts',
'/admin/alerts/alert-deliveries',
'/admin/alerts/alert-rules',
'/admin/alerts/alert-destinations',
'/admin/settings/workspace',
];
foreach ($workspaceHubPaths as $path) {
expect(AdminSurfaceScope::fromPath($path))->toBe(AdminSurfaceScope::WorkspaceWideSurface, $path);
}
$workspaceOwnedAnalysisPaths = [
'/admin/baseline-profiles',
'/admin/baseline-profiles/42',
'/admin/baseline-profiles/42/edit',
'/admin/baseline-profiles/42/compare-matrix',
'/admin/baseline-snapshots',
'/admin/baseline-snapshots/42',
'/admin/findings/my-work',
'/admin/findings/intake',
'/admin/findings/hygiene',
'/admin/cross-environment-compare',
];
foreach ($workspaceOwnedAnalysisPaths as $path) {
expect(AdminSurfaceScope::fromPath($path))->toBe(AdminSurfaceScope::WorkspaceOwnedAnalysisSurface, $path);
}
$environmentOwnedPaths = [
'/admin/workspaces/acme/environments/prod',
'/admin/workspaces/acme/environments/prod/baseline-compare',
'/admin/workspaces/acme/environments/prod/required-permissions',
'/admin/workspaces/acme/environments/prod/inventory',
'/admin/workspaces/acme/environments/prod/inventory/inventory-coverage',
'/admin/workspaces/acme/environments/prod/diagnostics',
];
foreach ($environmentOwnedPaths as $path) {
expect(AdminSurfaceScope::fromPath($path))->toBe(AdminSurfaceScope::EnvironmentBound, $path);
}
});
it('workspace_hub_clean_urls_never_emit_environment_or_legacy_query_params', function (): void {
$environment = ManagedEnvironment::factory()->active()->create();
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
$workspace = $environment->workspace()->firstOrFail();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$cleanUrls = [
OperationRunLinks::index(workspace: $workspace),
ProviderConnectionResource::getUrl('index', panel: 'admin'),
FindingExceptionsQueue::getUrl(panel: 'admin'),
route('admin.evidence.overview'),
route('filament.admin.pages.reviews'),
route('filament.admin.pages.reviews.workspace'),
GovernanceInbox::getUrl(panel: 'admin'),
DecisionRegister::getUrl(panel: 'admin'),
route('admin.monitoring.audit-log'),
route('filament.admin.alerts'),
AlertDeliveryResource::getUrl('index', panel: 'admin'),
route('filament.admin.alerts.resources.alert-rules.index'),
route('filament.admin.alerts.resources.alert-destinations.index'),
route('filament.admin.pages.settings.workspace'),
];
foreach ($cleanUrls as $url) {
expect($url)->not->toContain('environment_id=', $url)
->and($url)->not->toContain('tenant=', $url)
->and($url)->not->toContain('tenant_id=', $url)
->and($url)->not->toContain('managed_environment_id=', $url)
->and($url)->not->toContain('environment=', $url)
->and($url)->not->toContain('tenant_scope=', $url)
->and($url)->not->toContain('tableFilters', $url)
->and(WorkspaceHubRegistry::hasForbiddenQuery($url))->toBeFalse($url);
}
});
it('clear_filter_results_match_clean_workspace_hub_entry_for_filterable_hubs', function (): void {
$environment = ManagedEnvironment::factory()->active()->create();
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
$workspace = $environment->workspace()->firstOrFail();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$dirtyQuery = [
'environment_id' => (int) $environment->getKey(),
'tenant' => (string) $environment->external_id,
'tenant_id' => (int) $environment->getKey(),
'managed_environment_id' => (int) $environment->getKey(),
'environment' => (string) $environment->getRouteKey(),
'tenant_scope' => 'environment',
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $environment->getKey()],
],
'activeTab' => 'failed',
];
$cases = [
OperationRunLinks::index(workspace: $workspace).'?'.http_build_query($dirtyQuery),
ProviderConnectionResource::getUrl('index', $dirtyQuery, panel: 'admin'),
FindingExceptionsQueue::getUrl(panel: 'admin', parameters: $dirtyQuery),
route('admin.evidence.overview', $dirtyQuery),
GovernanceInbox::getUrl(panel: 'admin', parameters: $dirtyQuery),
DecisionRegister::getUrl(panel: 'admin', parameters: $dirtyQuery),
route('admin.monitoring.audit-log', $dirtyQuery),
AlertDeliveryResource::getUrl('index', $dirtyQuery, panel: 'admin'),
];
foreach ($cases as $dirtyUrl) {
$cleanUrl = WorkspaceHubRegistry::cleanUrl($dirtyUrl);
$query = [];
parse_str((string) parse_url($cleanUrl, PHP_URL_QUERY), $query);
expect(WorkspaceHubRegistry::hasForbiddenQuery($cleanUrl))->toBeFalse($cleanUrl)
->and($query)->toHaveKey('activeTab', 'failed');
}
});
it('environment_id_filters_reject_cross_workspace_environment_ids', function (): void {
$environment = ManagedEnvironment::factory()->active()->create();
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
$workspace = $environment->workspace()->firstOrFail();
$foreignEnvironment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec322 Foreign Environment',
]);
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$urls = [
route('admin.operations.index', [
'workspace' => $workspace,
'environment_id' => (int) $foreignEnvironment->getKey(),
]),
ProviderConnectionResource::getUrl('index', [
'environment_id' => (int) $foreignEnvironment->getKey(),
], panel: 'admin'),
AlertDeliveryResource::getUrl('index', [
'environment_id' => (int) $foreignEnvironment->getKey(),
], panel: 'admin'),
route('admin.monitoring.audit-log', [
'environment_id' => (int) $foreignEnvironment->getKey(),
]),
];
foreach ($urls as $url) {
$this->get($url)->assertNotFound();
}
});

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Pages\Settings\WorkspaceSettings;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDestinationResource;
use App\Filament\Resources\AlertRuleResource;
use App\Filament\Resources\ProviderConnectionResource;
use App\Models\ManagedEnvironment;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationRunLinks;
it('environment_cta_urls_use_the_correct_surface_contract', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec322 CTA Environment',
]);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
$workspace = $environment->workspace()->firstOrFail();
$this->actingAs($user);
setAdminPanelContext($environment);
$workspaceHubUrls = [
OperationRunLinks::index($environment),
ManagedEnvironmentLinks::operationsUrl($environment),
ManagedEnvironmentLinks::providerConnectionsUrl($environment),
ProviderConnectionResource::getUrl('index', ['environment_id' => (int) $environment->getKey()], panel: 'admin'),
CustomerReviewWorkspace::environmentFilterUrl($environment),
GovernanceInbox::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]),
DecisionRegister::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]),
FindingExceptionsQueue::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]),
route('admin.evidence.overview', ['environment_id' => (int) $environment->getKey()]),
ReviewRegister::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]),
route('admin.monitoring.audit-log', ['environment_id' => (int) $environment->getKey()]),
AlertDeliveryResource::getUrl('index', ['environment_id' => (int) $environment->getKey()], panel: 'admin'),
];
foreach ($workspaceHubUrls as $url) {
$query = spec322Query($url);
expect($query)->toHaveKey('environment_id', (string) $environment->getKey())
->and($query)->not->toHaveKey('tenant')
->and($query)->not->toHaveKey('tenant_id')
->and($query)->not->toHaveKey('managed_environment_id')
->and($query)->not->toHaveKey('environment')
->and($query)->not->toHaveKey('tenant_scope')
->and($query)->not->toHaveKey('tableFilters');
}
$environmentOwnedUrls = [
ManagedEnvironmentLinks::viewUrl($environment),
ManagedEnvironmentLinks::baselineCompareUrl($environment),
BaselineCompareLanding::getUrl(panel: 'admin', tenant: $environment),
ManagedEnvironmentLinks::requiredPermissionsUrl($environment),
ManagedEnvironmentLinks::diagnosticsUrl($environment),
route('filament.admin.workspaces.{workspace}.environments.{environment}.inventory', [
'workspace' => ManagedEnvironmentLinks::workspaceRouteKey($workspace),
'environment' => ManagedEnvironmentLinks::environmentRouteKey($environment),
]),
route('filament.admin.workspaces.{workspace}.environments.{environment}.inventory.pages.inventory-coverage', [
'workspace' => ManagedEnvironmentLinks::workspaceRouteKey($workspace),
'environment' => ManagedEnvironmentLinks::environmentRouteKey($environment),
]),
];
foreach ($environmentOwnedUrls as $url) {
expect((string) parse_url($url, PHP_URL_PATH))
->toContain('/admin/workspaces/'.ManagedEnvironmentLinks::workspaceRouteKey($workspace).'/environments/'.ManagedEnvironmentLinks::environmentRouteKey($environment))
->and(spec322Query($url))->not->toHaveKey('environment_id')
->and(spec322Query($url))->not->toHaveKey('managed_environment_id')
->and(spec322Query($url))->not->toHaveKey('tenant')
->and(spec322Query($url))->not->toHaveKey('tableFilters');
}
$workspaceConfigurationUrls = [
AlertRuleResource::getUrl('index', panel: 'admin'),
AlertDestinationResource::getUrl('index', panel: 'admin'),
WorkspaceSettings::getUrl(panel: 'admin'),
];
foreach ($workspaceConfigurationUrls as $url) {
expect(spec322Query($url))->not->toHaveKey('environment_id')
->and(spec322Query($url))->not->toHaveKey('managed_environment_id')
->and(spec322Query($url))->not->toHaveKey('tenant')
->and(spec322Query($url))->not->toHaveKey('tableFilters');
}
});
/**
* @return array<string, mixed>
*/
function spec322Query(string $url): array
{
$query = [];
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
return $query;
}

View File

@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Models\AlertDelivery;
use App\Models\AlertDestination;
use App\Models\AlertRule;
use App\Models\AuditLog as AuditLogModel;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Route;
use Livewire\Livewire;
it('legacy_environment_query_aliases_do_not_create_filter_or_shell_state', function (): void {
[$user, $environmentA, $environmentB, $records] = spec322LegacyAliasFixture();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
$legacyQueries = [
'tenant' => ['tenant' => (string) $environmentA->getKey()],
'tenant_id' => ['tenant_id' => (int) $environmentA->getKey()],
'managed_environment_id' => ['managed_environment_id' => (int) $environmentA->getKey()],
'environment' => ['environment' => (string) $environmentA->getRouteKey()],
'tenant_scope' => ['tenant_scope' => 'environment'],
'tableFilters' => [
'tableFilters' => [
'managed_environment_id' => ['value' => (string) $environmentA->getKey()],
],
],
];
foreach ($legacyQueries as $query) {
Livewire::withQueryParams($query)
->actingAs($user)
->test(Operations::class)
->assertSet('tableFilters.managed_environment_id.value', null)
->assertDontSee('Environment filter:')
->assertCanSeeTableRecords([$records['runA'], $records['runB']]);
Livewire::withQueryParams($query)
->actingAs($user)
->test(ListAlertDeliveries::class)
->assertSet('tableFilters.managed_environment_id.value', null)
->assertDontSee('Environment filter:')
->assertCanSeeTableRecords([$records['deliveryA'], $records['deliveryB']]);
Livewire::withQueryParams($query)
->actingAs($user)
->test(AuditLogPage::class)
->assertSet('tableFilters.managed_environment_id.value', null)
->assertDontSee('Environment filter:')
->assertCanSeeTableRecords([$records['auditA'], $records['auditB'], $records['auditWorkspace']]);
}
});
it('has_no_active_legacy_tenant_panel_routes', function (): void {
$legacyRouteUris = collect(Route::getRoutes())
->map(fn ($route): string => ltrim((string) $route->uri(), '/'))
->filter(fn (string $uri): bool => preg_match('#^admin/t(?:/|$)#', $uri) === 1)
->values();
$registeredProviders = require base_path('bootstrap/providers.php');
$tenantPanelProviders = collect($registeredProviders)
->filter(fn (string $provider): bool => str_contains($provider, 'TenantPanelProvider'))
->values();
expect($legacyRouteUris)->toBeEmpty()
->and($tenantPanelProviders)->toBeEmpty()
->and(file_exists(app_path('Providers/Filament/TenantPanelProvider.php')))->toBeFalse()
->and(file_exists(app_path('Filament/Providers/TenantPanelProvider.php')))->toBeFalse()
->and(Filament::getPanel('tenant'))->toBeNull();
$this->get('/admin/t/example')->assertNotFound();
});
it('allows_tenant_terms_only_in_provider_boundary_contexts', function (): void {
$files = spec322LegacyGuardFiles([
base_path('app/Support/Navigation'),
base_path('app/Filament/Pages/Monitoring/Operations.php'),
base_path('app/Filament/Pages/Monitoring/FindingExceptionsQueue.php'),
base_path('app/Filament/Pages/Governance/GovernanceInbox.php'),
base_path('app/Filament/Pages/Governance/DecisionRegister.php'),
base_path('app/Filament/Pages/Monitoring/EvidenceOverview.php'),
base_path('app/Filament/Pages/Reviews/ReviewRegister.php'),
base_path('app/Filament/Pages/Reviews/CustomerReviewWorkspace.php'),
base_path('app/Filament/Resources/ProviderConnectionResource.php'),
base_path('app/Filament/Resources/ProviderConnectionResource/Pages/ListProviderConnections.php'),
base_path('routes/web.php'),
]);
$hits = spec322LegacyPatternHits($files, [
'/\btenantPrefilterUrl\s*\(/',
'/\bCanonicalAdminTenantFilterState\b/',
'/\bWorkspaceScopedTenantRoutes\b/',
'/\bTenantPageCategory\b/',
'/\bEnsureFilamentTenantSelected\b/',
'/'.'ensure-filament-'.'tenant-selected'.'/',
'/\blastTenantId\s*\(/',
'/\brememberedTenant\s*\(/',
'/\brememberTenantContext\s*\(/',
'/\bLAST_TENANT_IDS_SESSION_KEY\b/',
'/\bTenantBound\b/',
'/\bTenantScopedEvidence\b/',
]);
expect($hits)->toBeEmpty("Retired Tenant platform-context terms remain:\n".implode("\n", $hits));
});
/**
* @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: array<string, mixed>}
*/
function spec322LegacyAliasFixture(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec322 Alias Environment A',
'external_id' => 'spec322-alias-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec322 Alias Environment B',
'external_id' => 'spec322-alias-environment-b',
]);
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
$rule = AlertRule::factory()->create(['workspace_id' => (int) $environmentA->workspace_id]);
$destination = AlertDestination::factory()->create(['workspace_id' => (int) $environmentA->workspace_id]);
$deliveryA = AlertDelivery::factory()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'managed_environment_id' => (int) $environmentA->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_FAILED,
]);
$deliveryB = AlertDelivery::factory()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'managed_environment_id' => (int) $environmentB->getKey(),
'alert_rule_id' => (int) $rule->getKey(),
'alert_destination_id' => (int) $destination->getKey(),
'status' => AlertDelivery::STATUS_SENT,
]);
$auditA = spec322LegacyAuditRecord($environmentA, 'Spec322 alias audit A');
$auditB = spec322LegacyAuditRecord($environmentB, 'Spec322 alias audit B');
$auditWorkspace = spec322LegacyAuditRecord(null, 'Spec322 workspace audit', [
'workspace_id' => (int) $environmentA->workspace_id,
]);
return [$user, $environmentA, $environmentB, compact(
'runA',
'runB',
'deliveryA',
'deliveryB',
'auditA',
'auditB',
'auditWorkspace',
)];
}
/**
* @param array<string, mixed> $attributes
*/
function spec322LegacyAuditRecord(?ManagedEnvironment $environment, string $summary, array $attributes = []): AuditLogModel
{
$workspaceId = array_key_exists('workspace_id', $attributes)
? (int) $attributes['workspace_id']
: (int) ($environment?->workspace_id);
return AuditLogModel::query()->create(array_merge([
'workspace_id' => $workspaceId,
'managed_environment_id' => $environment?->getKey(),
'actor_email' => 'spec322@example.test',
'actor_name' => 'Spec322 Operator',
'action' => 'operation.completed',
'status' => 'success',
'resource_type' => 'operation_run',
'resource_id' => '322',
'summary' => $summary,
'metadata' => [],
'recorded_at' => now(),
], $attributes));
}
/**
* @param list<string> $roots
* @return list<string>
*/
function spec322LegacyGuardFiles(array $roots): array
{
$files = [];
foreach ($roots as $root) {
if (is_file($root)) {
$files[] = $root;
continue;
}
if (! is_dir($root)) {
continue;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
);
foreach ($iterator as $file) {
if (! $file instanceof SplFileInfo || ! $file->isFile()) {
continue;
}
if (! in_array($file->getExtension(), ['php', 'md'], true)) {
continue;
}
$files[] = $file->getPathname();
}
}
sort($files);
return array_values(array_unique($files));
}
/**
* @param list<string> $files
* @param list<string> $patterns
* @return list<string>
*/
function spec322LegacyPatternHits(array $files, array $patterns): array
{
$hits = [];
foreach ($files as $path) {
$contents = file_get_contents($path);
if (! is_string($contents)) {
continue;
}
$lines = preg_split('/\R/', $contents) ?: [];
foreach ($patterns as $pattern) {
foreach ($lines as $lineNumber => $line) {
if (preg_match($pattern, $line) !== 1) {
continue;
}
$hits[] = str_replace(repo_path().'/', '', $path).':'.($lineNumber + 1).' -> '.trim($line);
}
}
}
return $hits;
}

View File

@ -0,0 +1,59 @@
# Specification Quality Checklist: Browser No-Drift Regression Guard
**Purpose**: Validate preparation completeness and quality before implementation.
**Created**: 2026-05-17
**Feature**: `specs/322-browser-no-drift-regression-guard/spec.md`
## Content Quality
- [x] Spec Candidate Check is completed.
- [x] The selected candidate is direct user-provided manual promotion.
- [x] Related completed specs are treated as historical context and not rewritten.
- [x] The scope is focused on durable guards and no-drift regression coverage.
- [x] No application implementation is included in preparation artifacts.
- [x] No migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is planned.
## Requirement Completeness
- [x] No `[NEEDS CLARIFICATION]` markers remain.
- [x] Functional requirements are testable and unambiguous.
- [x] Non-functional requirements cover browser stability, lane cost, fixture cost, and no Playwright MCP dependency.
- [x] Success criteria are measurable by artifacts and test results.
- [x] All user stories include independent test descriptions and acceptance scenarios.
- [x] Edge cases cover cross-workspace IDs, legacy aliases, stale state, back/forward, old clean URLs, and browser blockers.
- [x] Dependencies and assumptions are identified.
## Feature Readiness
- [x] `spec.md` exists.
- [x] `plan.md` exists.
- [x] `tasks.md` exists.
- [x] `test-plan.md` exists.
- [x] `coverage-manifest.md` exists.
- [x] Screenshots artifact directory is represented by `.gitkeep`.
- [x] Tasks are ordered, small, and verifiable.
- [x] Tasks include Feature/static guards and Browser guard coverage.
- [x] Tasks include validation commands and final report obligations.
- [x] Tasks include explicit non-goals and stop conditions.
## Constitution / Repo Alignment
- [x] Workspace isolation and cross-workspace rejection are explicit.
- [x] Legacy Tenant aliases remain invalid and no compatibility layer is allowed.
- [x] Provider Tenant terminology remains provider-boundary only.
- [x] Proportionality review covers the new test/manifest ownership cost.
- [x] Test governance names Browser lane expansion and fixture/helper cost controls.
- [x] Filament v5 / Livewire v4 compliance is explicitly addressed.
- [x] Provider registration location remains `apps/platform/bootstrap/providers.php`; no provider change is planned.
- [x] Global search and destructive action output-contract points are addressed.
- [x] Asset strategy is explicit; no `filament:assets` deployment change is planned.
## Preparation Analysis Outcome
- [x] Preparation artifacts are internally consistent after review.
- [x] No preparation issue requires application implementation.
- [x] No open question blocks implementation.
## Notes
Runtime acceptance remains pending until implementation, targeted Feature/static tests, targeted Browser tests, formatting, and `git diff --check` are completed.

View File

@ -0,0 +1,65 @@
# Spec 322 Coverage Manifest
This manifest is the review surface for durable Workspace / Environment no-drift coverage.
Coverage statuses:
- `existing`: already covered by a related test before Spec 322.
- `spec322`: covered by the Spec 322 implementation.
- `spec322-partial`: partially covered by Spec 322 browser coverage with deterministic Feature/static fallback.
- `gap`: browser coverage is blocked or intentionally excluded; notes must explain the deterministic fallback.
Spec 322 browser fixture boundary: browser tests create one workspace, two active Managed Environments, one user with workspace/environment membership, and only the records needed to prove visible/hidden scope across operations, provider connections, evidence, alerts, audit log, governance, reviews, baselines, findings, and settings. Exhaustive surface permutations stay in Feature/static guards.
| Surface | Classification | Clean URL | Filtered URL supported? | Environment route required? | Clear supported? | Browser covered? | Feature covered? | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Operations | workspace_hub | `/admin/workspaces/{workspace}/operations` | yes | no | yes | spec322-partial | spec322 | Spec322 browser covers clean entry/reload; filtered clear/history remains covered by existing Spec316 browser and Spec322 Feature guards. |
| Governance Inbox | workspace_hub | `/admin/governance/inbox` | yes | no | yes | spec322-partial | spec322 | Spec322 browser covers clean entry/reload; existing Spec314/316 browser plus Spec322 Feature guards cover filtered permutations. |
| Decision Register | workspace_hub | `/admin/governance/decisions` | yes | no | yes | existing | spec322 | Existing Spec314/316 browser coverage remains the browser anchor; Spec322 Feature guards include classification and URL contracts. |
| Finding Exceptions Queue | workspace_hub | `/admin/finding-exceptions/queue` | yes | no | yes | existing | spec322 | Existing Spec314/316 browser coverage remains the browser anchor; Spec322 representative legacy alias browser guard includes this surface. |
| Provider Connections | workspace_hub | `/admin/provider-connections` | yes | no | yes | spec322 | spec322 | Spec322 browser covers clean, filtered chip, clear, reload, back/forward, and representative legacy alias behavior. |
| Evidence Overview | workspace_hub | `/admin/evidence/overview` | yes | no | yes | spec322 | spec322 | Spec322 browser covers clean, filtered chip, clear, reload, and back/forward behavior. |
| Reviews | workspace_hub | `/admin/reviews` | yes | no | yes | existing | spec322 | Existing Spec314/316 browser coverage remains the browser anchor; Spec322 Feature guards include classification and URL contracts. |
| Customer Reviews | workspace_hub | `/admin/reviews/workspace` | yes | no | yes | existing | spec322 | Existing Spec314/316 browser coverage remains the browser anchor; Spec322 Feature guards include classification and URL contracts. |
| Alert Deliveries | workspace_hub | `/admin/alerts/alert-deliveries` | yes | no | yes | spec322 | spec322 | Spec322 browser covers filtered chip, clear, reload, and back/forward alignment. |
| Audit Log | workspace_hub | `/admin/audit-log` | yes | no | yes | spec322-partial | spec322 | Spec322 browser covers filtered and clean/reload shell state; clear/back-forward remains covered by Spec321/Spec322 Feature guards because browser click timing was unstable on this page. |
| Alerts Overview | workspace_hub | `/admin/alerts` | yes | no | yes | spec322-partial | spec322 | Spec322 browser covers clean and filtered shell state; clear contract remains covered by Spec321/Spec322 Feature guards. |
| Environment Dashboard | environment_owned_page | `/admin/workspaces/{workspace}/environments/{environment}` | no | yes | no | spec322 | spec322 | Browser covers Environment-owned route/shell and reload. |
| Baseline Compare | environment_owned_page | `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | no | yes | no | spec322 | spec322 | Browser covers Environment-owned route/shell, reload, and old workspace-style invalid access. |
| Required Permissions | environment_owned_page | `/admin/workspaces/{workspace}/environments/{environment}/required-permissions` | no | yes | no | spec322 | spec322 | Browser covers route/shell and reload; Feature guards cover URL contract. |
| Provider Readiness / Onboarding Readiness | environment_owned_page | Environment Dashboard readiness section | no | yes | no | spec322-partial | spec322 | No separate route was confirmed; Environment Dashboard browser smoke and Feature URL/scope guards are the deterministic fallback. |
| Inventory / Inventory Coverage | environment_owned_page | `/admin/workspaces/{workspace}/environments/{environment}/inventory` and `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` | no | yes | no | gap | spec322 | Browser coverage intentionally excluded for runtime cost; Feature URL/scope guards cover the route contract. |
| Environment Diagnostics | environment_owned_page | `/admin/workspaces/{workspace}/environments/{environment}/diagnostics` | no | yes | no | gap | spec322 | Browser coverage intentionally excluded for runtime cost; Feature URL/scope guards cover the route contract. |
| Baseline Profiles | workspace_owned_analysis_surface | `/admin/baseline-profiles` | no unless explicitly supported | no | no | spec322 | spec322 | Browser covers Workspace-only shell cutover from Environment origin plus reload. |
| Baseline Snapshots | workspace_owned_analysis_surface | `/admin/baseline-snapshots` | no unless explicitly supported | no | no | gap | spec322 | Browser coverage intentionally excluded for runtime cost; Feature classification and clean URL guards cover the contract. |
| My Findings | workspace_owned_analysis_surface | `/admin/findings/my-work` | no unless explicitly supported | no | no | spec322 | spec322 | Browser covers Workspace-only shell cutover from Environment origin plus reload. |
| Findings Intake | workspace_owned_analysis_surface | `/admin/findings/intake` | no unless explicitly supported | no | no | gap | spec322 | Browser coverage intentionally excluded for runtime cost; Feature classification and clean URL guards cover the contract. |
| Findings Hygiene | workspace_owned_analysis_surface | `/admin/findings/hygiene` | no unless explicitly supported | no | no | gap | spec322 | Browser coverage intentionally excluded for runtime cost; Feature classification and clean URL guards cover the contract. |
| Cross-environment Compare | workspace_owned_analysis_surface | `/admin/cross-environment-compare` | no unless explicitly supported | no | no | spec322 | spec322 | Browser covers Workspace-only shell cutover from Environment origin plus reload. |
| Alert Rules | workspace_configuration_surface | `/admin/alerts/alert-rules` | no | no | no | spec322 | spec322 | Browser covers stray `environment_id` rejection with no chip/shell. |
| Alert Destinations | workspace_configuration_surface | `/admin/alerts/alert-destinations` | no | no | no | spec322 | spec322 | Browser covers stray `environment_id` rejection with no chip/shell. |
| Workspace Settings | workspace_configuration_surface | `/admin/settings/workspace` | no | no | no | spec322 | spec322 | Browser covers Workspace-only shell cutover from Environment origin plus reload. |
| `/admin/t` legacy routes | out_of_scope | N/A | no | no | no | not applicable | spec322 | Spec322 Feature guard verifies no active legacy routes or TenantPanelProvider. |
| TenantPanelProvider | out_of_scope | N/A | no | no | no | not applicable | spec322 | Spec322 Feature guard verifies no runtime tenant panel registration or provider file. |
| Provider-boundary Tenant terminology | out_of_scope | N/A | no | no | no | not applicable | spec322 | Spec322 Feature guard checks retired platform-context Tenant terminology does not return outside allowed provider-boundary contexts. |
## Optional Surfaces
| Surface | Classification | Clean URL | Filtered URL supported? | Environment route required? | Clear supported? | Browser covered? | Feature covered? | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Notification Routing | workspace_configuration_surface | Not confirmed in Spec 322 prep | no unless explicitly supported | no | no | gap | gap | Optional; excluded until route/surface is confirmed. |
| Operational Controls | workspace_configuration_surface | Not confirmed in Spec 322 prep | no unless explicitly supported | no | no | gap | gap | Optional; excluded until route/surface is confirmed. |
| Customer Health | workspace_hub | Not confirmed in Spec 322 prep | not confirmed | no | not confirmed | gap | gap | Optional; not part of required Spec 322 surface list. |
| Stored Reports | environment_owned_page or workspace_hub depending route | Not confirmed in Spec 322 prep | not confirmed | route-specific | not confirmed | gap | existing | Existing environment route exclusions exist; not required for Spec 322 unless implementation discovers current drift. |
| Support Requests | out_of_scope | Not confirmed in Spec 322 prep | no | no | no | gap | existing | Modal/action support surface, not a primary no-drift browser target. |
| Review Packs / Exports | out_of_scope | Not confirmed in Spec 322 prep | no | route-specific | no | gap | existing | Artifact/download flows remain outside this guard unless linked from required surfaces. |
| Control Catalog | workspace_configuration_surface | Not confirmed in Spec 322 prep | no | no | no | gap | gap | Optional and excluded pending route/surface confirmation. |
## Required Gap Handling
If any planned browser coverage cannot be executed because of local browser profile lock, unavailable fixture, route unreachability, missing seeded data, or external process conflict:
1. Keep or add deterministic Feature/Unit coverage for the same product contract.
2. Mark the row `gap` or `spec322-partial` with exact reason.
3. Do not claim full browser coverage for that surface.
4. Include the blocker in the final implementation report.

View File

@ -0,0 +1,314 @@
# Implementation Plan: Browser No-Drift Regression Guard
**Branch**: `322-browser-no-drift-regression-guard`
**Date**: 2026-05-17
**Spec**: `specs/322-browser-no-drift-regression-guard/spec.md`
**Status**: Draft
## Summary
Create durable no-drift regression coverage for the Workspace / Environment contracts stabilized by Specs 314 through 321. The implementation is test/guard-first and must not add product behavior except narrow fixes required to make existing contracts deterministic.
The plan adds:
- Feature/static guards for surface classification, URL generation, canonical `environment_id`, clear-state contracts, cross-workspace rejection, legacy aliases, `/admin/t`, Tenant panel provider absence, and provider-boundary Tenant allowlist.
- Pest Browser smoke tests grouped by surface model for shell, chip, clear, reload, and back/forward alignment.
- A coverage manifest and test plan so future admin surfaces can update coverage deliberately.
## Technical Context
**Language / Version**: PHP 8.4.15
**Primary Framework**: Laravel 12.52.0
**Admin UI**: Filament 5.2.1
**Reactive Layer**: Livewire 4.1.4
**Database**: PostgreSQL via Sail
**Testing**: Pest 4.3.1, PHPUnit 12.5
**Validation Lanes**: Feature/static guards, Browser lane, possible heavy-governance classification if discovery-style guards are broadened
**Target Platform**: Laravel monolith in `apps/platform`
**Project Type**: Web application
**Performance Goals**: Keep Browser tests focused and grouped so the lane remains stable and reviewable.
**Constraints**: No migrations, no seeders unless fixture determinism is impossible, no package changes, no env/queue/scheduler/storage changes, no Playwright MCP dependency, no backwards compatibility aliases.
**Scale / Scope**: Core admin surface contracts only; optional surfaces are documented rather than silently included.
Package posture:
- Filament v5 requires Livewire v4.0+; this repo uses Livewire 4.1.4.
- Laravel 12 panel providers are registered in `apps/platform/bootstrap/providers.php`; this spec does not add a panel provider.
- No frontend assets are added. No `filament:assets` deployment change is expected.
## Current Repo Truth
Relevant existing seams:
```text
apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php
apps/platform/app/Support/Navigation/AdminSurfaceScope.php
apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php
apps/platform/app/Filament/Concerns/ClearsWorkspaceHubEnvironmentFilterState.php
apps/platform/resources/views/filament/partials/workspace-hub-environment-filter-chip.blade.php
apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php
apps/platform/app/Support/ManagedEnvironmentLinks.php
apps/platform/app/Support/OperationRunLinks.php
apps/platform/routes/web.php
```
Existing related tests and smoke coverage:
```text
apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubSidebarUrlContractTest.php
apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php
apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php
apps/platform/tests/Feature/Guards/NoLegacyTenantPanelRuntimeTest.php
apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php
apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php
apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php
apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php
```
Spec 322 should extend or add targeted files rather than replacing these proven anchors.
## Project Structure
### Spec Artifacts
```text
specs/322-browser-no-drift-regression-guard/spec.md
specs/322-browser-no-drift-regression-guard/plan.md
specs/322-browser-no-drift-regression-guard/tasks.md
specs/322-browser-no-drift-regression-guard/test-plan.md
specs/322-browser-no-drift-regression-guard/coverage-manifest.md
specs/322-browser-no-drift-regression-guard/checklists/requirements.md
specs/322-browser-no-drift-regression-guard/artifacts/screenshots/.gitkeep
```
### Likely Runtime/Test Areas For Later Implementation
```text
apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php
apps/platform/tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php
apps/platform/tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php
apps/platform/tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php
apps/platform/tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php
apps/platform/tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php
apps/platform/tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php
apps/platform/tests/Browser/Support/Spec322WorkspaceEnvironmentBrowserHarness.php
```
The support harness path is suggested, not mandatory. If existing helper conventions provide enough reuse, implementation should keep helpers local to the test files.
## UI / Surface Guardrail Plan
- **Guardrail scope**: Workflow-only guardrail over existing surfaces.
- **Native vs custom classification summary**: Existing Filament-native surfaces; no new product UI.
- **Shared-family relevance**: Navigation, shell, filter chip, clear/reset, browser history.
- **State layers in scope**: route path, query string, Livewire properties, Filament table filters, deferred filters, persisted/session filters, shell context, chip visibility, browser history.
- **Audience modes in scope**: operator-MSP and support-platform only as existing users; no new disclosure modes.
- **Decision/diagnostic/raw hierarchy plan**: N/A - guard assertions only.
- **Raw/support gating plan**: N/A.
- **One-primary-action / duplicate-truth control**: Existing clear action remains the only filter reset control under test.
- **Handling modes by drift class or surface**: Workspace/Environment mismatch is a hard-stop candidate for implementation; optional unreachable browser surfaces are document-in-feature only if Feature coverage proves the contract.
- **Repository-signal treatment**: Review-mandatory for new admin surfaces missing manifest/test coverage.
- **Special surface test profiles**: `global-context-shell`.
- **Required tests or manual smoke**: Feature/static guards plus Pest Browser smoke tests.
- **Exception path and spread control**: Any browser gap must be documented in `coverage-manifest.md` with deterministic non-browser proof.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: Navigation registry, surface scope enum, Environment filter resolver, filter resetter, shell context, browser tests.
- **Shared abstractions reused**: `WorkspaceHubRegistry`, `AdminSurfaceScope`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `ClearsWorkspaceHubEnvironmentFilterState`, shared filter chip partial.
- **New abstraction introduced? why?**: No runtime abstraction. Test-only helper extraction is allowed only when it reduces repeated fragile browser setup.
- **Why the existing abstraction was sufficient or insufficient**: Runtime abstractions are sufficient; current gap is durable cross-surface guard coverage.
- **Bounded deviation / spread control**: Test helpers must remain in test support or local browser test scope and must not become product navigation registries.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no
- **Central contract reused**: N/A
- **Delegated UX behaviors**: N/A
- **Surface-owned behavior kept local**: Existing Operations hub may be used as a workspace hub test surface.
- **Queued DB-notification policy**: N/A
- **Terminal notification path**: N/A
- **Exception path**: none
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes, guard-only
- **Provider-owned seams**: Microsoft/Entra/provider Tenant identity terms remain allowed through Spec 317 allowlist.
- **Platform-core seams**: Workspace, Managed Environment, Environment filter query key, shell ownership, route ownership.
- **Neutral platform terms / contracts preserved**: Workspace, Managed Environment, Environment, Provider Connection.
- **Retained provider-specific semantics and why**: Provider Tenant terminology remains where it means external Microsoft/Entra tenant identity.
- **Bounded extraction or follow-up path**: none expected; implementation should add a follow-up only if a current provider-boundary term is ambiguous outside the allowlist.
## Constitution Check
### Pre-Implementation
- **Inventory-first / Snapshots-second**: Pass. No inventory or snapshot truth changes.
- **Read/write separation**: Pass. No product write actions are introduced.
- **Graph contract path**: Pass. No Graph calls are introduced.
- **Deterministic capabilities**: Pass. Capability behavior is not changed.
- **Workspace isolation**: Pass with required tests. Cross-workspace Environment IDs must be rejected.
- **Tenant isolation / provider boundary**: Pass with required legacy alias and provider Tenant allowlist guards.
- **RBAC-UX**: Pass with required tests for non-member/unauthorized Environment behavior where applicable.
- **OperationRun UX**: N/A.
- **Test governance**: Requires explicit Browser lane expansion, manifest, and test-plan documentation.
- **Proportionality**: Pass. No runtime persistence, enum/status family, or runtime framework. Test-only helper extraction must stay narrow.
- **Shared Pattern First**: Pass. Existing runtime paths are reused.
- **Filament-native UI**: Pass. No product UI changes; assertions target existing native/shared surfaces.
- **Spec Candidate Gate**: Pass. Direct manual promotion and already referenced follow-up from completed specs.
### Post-Design
No constitutional violation is expected.
If implementation discovers that passing the guards requires product behavior changes beyond a narrow fix to existing Specs 314 through 321 contracts, update `spec.md` and `plan.md` before continuing.
If implementation adds a runtime registry, persisted entity, enum, or broad helper layer, reopen the proportionality review and stop for review.
## Surface Classification Plan
Source classification comes from existing runtime contracts plus Spec 318 audit terminology.
| Classification | Runtime contract |
| --- | --- |
| `workspace_hub` | Workspace-only shell; optional explicit filter when supported |
| `environment_owned_page` | Workspace + Environment shell; Environment route required |
| `workspace_owned_analysis_surface` | Workspace-only shell; no remembered Environment shell inheritance |
| `workspace_configuration_surface` | Workspace-only shell; configuration semantics; no Environment chip unless explicitly supported |
| `system_platform_surface` | System panel or non-admin platform surface |
| `out_of_scope` | Not stable/reachable or not part of this guard |
The manifest must use these labels and must be updated when future specs add admin surfaces.
## Feature / Static Guard Plan
Create or update focused tests for:
- `it('classifies_core_admin_surfaces_without_scope_drift')`
- `it('workspace_hub_clean_urls_never_emit_environment_or_legacy_query_params')`
- `it('environment_cta_urls_use_the_correct_surface_contract')`
- `it('clear_filter_results_match_clean_workspace_hub_entry_for_filterable_hubs')`
- `it('environment_id_filters_reject_cross_workspace_environment_ids')`
- `it('legacy_environment_query_aliases_do_not_create_filter_or_shell_state')`
- `it('has_no_active_legacy_tenant_panel_routes')`
- `it('allows_tenant_terms_only_in_provider_boundary_contexts')`
Use existing tests where possible and add Spec 322 coverage only for gaps.
## Browser Guard Plan
Use Pest Browser. Do not introduce Playwright MCP dependency.
Group browser tests by contract:
1. Workspace hub clean entry, reload, and sidebar/global/direct origins.
2. Workspace hub filtered entry, chip, clear, reload, and back/forward.
3. Environment-owned route/shell contract and invalid clean workspace access.
4. Workspace-owned analysis and workspace configuration shell cutover.
5. Alerts/Audit Log focused no-drift smoke.
Screenshots are diagnostic only. Store useful artifacts under:
```text
specs/322-browser-no-drift-regression-guard/artifacts/screenshots/
```
## Data / Migration Plan
No migrations are planned.
No new tables, persisted entities, seeders, packages, env vars, queues, scheduler, storage, deployment assets, or compatibility layers are planned.
If deterministic browser fixtures cannot be produced without a seeder change, update the spec and plan first. Prefer test factories and explicit fixture helpers.
## Authorization / Security Plan
- Use existing workspace and Managed Environment access helpers/factories.
- Assert cross-workspace and unauthorized Environment IDs do not leak data and do not switch Workspace.
- Keep UI state out of the authorization boundary; guard server-side behavior via Feature tests where possible.
- Keep `/admin/t` and `TenantPanelProvider` absence guarded.
## Filament v5 Output Contract
1. **Livewire v4.0+ compliance**: Required. The app uses Livewire 4.1.4; tests and any narrow fixes must not introduce Livewire v3 references.
2. **Provider registration location**: No provider changes are planned. Laravel 12 Filament panel providers remain in `apps/platform/bootstrap/providers.php`.
3. **Globally searchable resources**: Spec 322 should not make any resource globally searchable. Existing resources must continue either to have View/Edit pages or have global search disabled according to Filament v5 rules.
4. **Destructive actions**: Spec 322 introduces no destructive product actions. If a narrow runtime fix touches an existing destructive action, it must preserve `->action(...)`, `->requiresConfirmation()`, policy/gate authorization, and audit behavior.
5. **Asset strategy**: No global or on-demand Filament assets are planned. No deployment `filament:assets` change is required.
6. **Testing plan**: Feature/static guards plus Pest Browser tests for pages/resources as Livewire/browser-visible surfaces. Mutating actions are not introduced.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature/static for registries, URLs, aliases, and authorization; Browser for shell/chip/clear/reload/history.
- **Affected validation lanes**: fast-feedback, browser, and optional heavy-governance if discovery-style guard breadth expands.
- **Why this lane mix is the narrowest sufficient proof**: Feature tests can exhaustively prove deterministic contracts. Browser is required only for browser state and visible shell/chip/history.
- **Narrowest proving commands**: Listed in `test-plan.md`.
- **Fixture / helper / factory / seed / context cost risks**: Browser setup can grow. Keep helper defaults explicit and avoid global seed reliance.
- **Expensive defaults or shared helper growth introduced?**: Not by default. Any browser harness must be opt-in.
- **Heavy-family additions, promotions, or visibility changes**: Explicit `Spec322` Browser smoke files.
- **Surface-class relief / special coverage rule**: `global-context-shell`.
- **Closing validation and reviewer handoff**: Review manifest, test names, helper cost, focused commands, browser gap documentation, and absence of runtime compatibility aliases.
- **Budget / baseline / trend follow-up**: Document runtime if materially higher than existing Spec 314/316 browser smoke.
- **Review-stop questions**: Are browser tests focused? Are legacy aliases rejected rather than accepted? Are optional gaps documented? Does any helper create hidden fixture cost?
- **Escalation path**: document-in-feature; follow-up-spec only if browser lane cost becomes structural.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
- **Why no dedicated follow-up spec is needed**: Spec 322 is the dedicated no-drift guardrail package.
## Implementation Phases
1. Confirm existing surface classifications and route/helper names.
2. Add/update coverage manifest with all required surfaces and current planned coverage.
3. Add Feature/static guards for classifications, URLs, legacy aliases, cross-workspace rejection, `/admin/t`, and provider Tenant allowlist.
4. Add or refactor test-only browser helper patterns if existing duplication becomes brittle.
5. Add grouped Pest Browser smoke tests for workspace hubs, Environment-owned pages, workspace-owned analysis/configuration surfaces, and Alerts/Audit Log.
6. Run focused Feature/static tests.
7. Run focused Browser tests.
8. Update coverage manifest with actual coverage and any browser gaps.
9. Run formatting and `git diff --check`.
10. Report exact test commands/results and no-runtime-change confirmations.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
| --- | --- | --- |
| Browser lane expansion | Shell/chip/clear/reload/history alignment is browser-visible and cannot be proven by static tests alone | Existing per-spec smoke tests do not cover the full stabilized chain or Alerts/Audit Log after Spec 321 |
| Coverage manifest | Future surfaces need an explicit coverage contract | Relying on test file names alone would hide browser gaps and classification drift |
## Rollout Considerations
- No runtime rollout impact is expected.
- No staging/production migration or deployment step is expected.
- CI should include the focused Feature/static guard command and the Browser lane command when Browser infrastructure is available.
- Do not claim full suite green unless the full suite is actually run.
## Risks And Controls
| Risk | Control |
| --- | --- |
| Browser suite becomes flaky or too broad | Group smoke tests by surface model, assert stable text/URL/chip state, move exhaustive alias coverage to Feature tests |
| Test helper hides expensive setup | Keep workspace/environment/session setup explicit and opt-in |
| Optional surfaces are silently skipped | Document gaps and reasons in `coverage-manifest.md` |
| Legacy aliases are accidentally accepted in tests | Feature guards must assert aliases do not create filter or shell state |
| Runtime behavior changes beyond guard fixes | Update spec/plan before continuing |
## Validation Plan
See `test-plan.md` for exact targeted commands.
Minimum final validation:
```bash
cd apps/platform
./vendor/bin/sail artisan test tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php --compact
./vendor/bin/sail artisan test tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php --compact
./vendor/bin/sail pint --dirty
git diff --check
```
Exact file names may be adjusted during implementation if existing repo conventions make narrower updates preferable.

View File

@ -0,0 +1,393 @@
# Feature Specification: Browser No-Drift Regression Guard
**Feature Branch**: `322-browser-no-drift-regression-guard`
**Created**: 2026-05-17
**Status**: Draft
**Input**: User supplied full Spec 322 draft: "Browser No-Drift Regression Guard"
**Type**: Durable regression coverage / browser contract guard / no-drift enforcement
**Runtime Posture**: Test/guard-first. No new product behavior unless required to make guards deterministic.
## Dependencies
- Spec 314: Workspace Hub Navigation Context Contract
- Spec 315: Environment CTA Explicit Filter Contract
- Spec 316: Workspace Hub Clear Filter Contract
- Spec 317: Legacy Tenant / Environment Context Cleanup
- Spec 318: Admin Surface Scope & Shell Context Audit
- Spec 319: Environment-Owned Surface Routing & Shell Context Contract
- Spec 320: Workspace-Owned Analysis Surface Registration & Shell Cutover
- Spec 321: Alerts / Audit Log Environment Filter Contract Decision
## Spec Candidate Check
- **Problem**: Workspace, Environment, filter, shell, clear-state, and legacy Tenant cleanup contracts now span routes, Filament pages/resources, Livewire state, session persistence, sidebar/global navigation, and browser history. Without durable guards, future specs can reintroduce hidden Environment shell inheritance or stale filter state.
- **Today's failure**: A future page or helper could emit `tenant`, `managed_environment_id`, remembered Environment state, or a workspace-clean URL that silently renders Environment-owned data. Operators would see mismatched shell, URL, filter chip, and data scope.
- **User-visible improvement**: Operators get consistent Workspace-only hubs, explicit Environment-owned pages, visible filtered state, reliable clear behavior, and no resurrected legacy Tenant platform context.
- **Smallest enterprise-capable version**: Add focused Feature/static guards plus grouped Pest Browser contract smoke tests for core high-risk surfaces, with a coverage manifest documenting any browser gaps.
- **Explicit non-goals**: No product redesign, no new Environment filters, no Alert/Audit contract change beyond Spec 321, no migrations, no seeders unless deterministic browser fixtures are impossible, no packages, no compatibility aliases, no Playwright MCP dependency, no visual-regression framework.
- **Permanent complexity imported**: New or updated Pest Feature tests, Pest Browser smoke tests, test-only helper structure where existing browser conventions are insufficient, coverage manifest, test plan, and ongoing maintenance obligation for new admin surfaces.
- **Why now**: Specs 314 through 321 intentionally stabilized the contracts and repeatedly deferred durable no-drift coverage to Spec 322. Spec 321 is now implemented on the current branch history, so the guard can cover the full chain.
- **Why not local**: Local tests per surface already exist, but drift risk is cross-surface and browser-state-dependent. A single future change can break URL/chip/shell/session/history alignment across several surfaces.
- **Approval class**: Core Enterprise
- **Red flags triggered**: Cross-surface coverage and test infrastructure. Defense: the scope is test-only and protects workspace isolation, Environment ownership, auditability, and operator trust without adding runtime product frameworking.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Candidate Source And Guardrail Result
**Candidate Source**: Direct user-provided manual promotion for Spec 322. The candidate is also referenced as deferred follow-up in Specs 319, 320, and 321.
**Completed-Spec Guardrail**: Specs 314 through 321 were inspected as completed or historical context and remain unchanged by this preparation. This package creates a new Spec 322 path and does not rewrite close-out, validation, completed tasks, smoke evidence, or review history from earlier specs.
**Close alternatives deferred**:
- Customer Review Workspace v1 polish: productization lane after context foundations are protected.
- Decision-Based Governance Inbox v1 polish: product lane after no-drift guards exist.
- Localization v1 and commercial lifecycle maturity: roadmap product lanes, not guardrail completion.
- Broad visual regression framework: deferred as too heavy for the current need.
## Summary
Add durable browser and regression coverage to prevent Workspace / Environment context drift from returning.
Specs 314 through 321 established these product contracts:
- Workspace hubs use Workspace-only shell ownership.
- Environment Dashboard CTAs use canonical `?environment_id=...` when they intentionally filter a workspace hub.
- Clear filter removes URL, Livewire, Filament table, deferred table, session, and persisted state.
- Legacy Tenant platform context is removed or quarantined.
- Environment-owned pages require Environment route/shell context.
- Workspace-owned analysis pages cut over to Workspace-only shell.
- Alerts and Audit Log have explicit Spec 321 contracts.
Spec 322 turns those contracts into maintainable guards.
## Product Context
TenantPilot is workspace-first.
Workspace is the primary platform context. Managed Environment is a secondary operational context inside a Workspace. Provider Tenant remains only a provider-boundary concept, such as Microsoft/Entra/Graph tenant identity.
The guarded admin surface models are:
| Model | Expected contract |
| --- | --- |
| Workspace Hub | Workspace-only shell, clean workspace-wide URL, optional canonical `environment_id` filter if supported, visible chip when filtered, clear returns to clean state |
| Environment-Owned Page | Workspace + Environment shell, route encodes Environment ownership, no clean workspace-only rendering, no workspace-hub `environment_id` model |
| Workspace-Owned Analysis Surface | Workspace-only shell, no remembered Environment inheritance, optional `environment_id` only where explicitly supported |
| Workspace Configuration Surface | Workspace-only shell, no Environment filter unless explicitly supported |
## Spec Scope Fields
- **Scope**: canonical-view / workspace shell and Environment route guardrail
- **Primary Routes**: `/admin`, `/admin/workspaces/{workspace}/operations`, `/admin/provider-connections`, `/admin/finding-exceptions/queue`, `/admin/evidence/overview`, `/admin/reviews`, `/admin/reviews/workspace`, `/admin/governance/inbox`, `/admin/governance/decisions`, `/admin/audit-log`, `/admin/alerts`, `/admin/alerts/alert-deliveries`, `/admin/alerts/alert-rules`, `/admin/alerts/alert-destinations`, `/admin/settings/workspace`, `/admin/workspaces/{workspace}/environments/{environment}`, Environment-owned subroutes, `/admin/baseline-profiles`, `/admin/baseline-snapshots`, `/admin/findings/my-work`, `/admin/findings/intake`, `/admin/findings/hygiene`, `/admin/cross-environment-compare`
- **Data Ownership**: No new persisted data. Guards must prove existing workspace-owned and Environment-owned records remain scoped by current Workspace and authorized Environment membership.
- **RBAC**: Tests must use existing workspace membership and Managed Environment access fixtures. Cross-workspace or unauthorized Environment IDs must resolve as safe no-access / deny-as-not-found behavior.
For canonical-view specs:
- **Default filter behavior when Environment context is active**: Workspace hubs and workspace-owned analysis surfaces must remain Workspace-only unless a valid canonical `environment_id` query parameter is present and the surface explicitly supports it.
- **Explicit entitlement checks preventing cross-environment leakage**: Feature tests must prove cross-workspace and unauthorized Environment IDs do not switch Workspace, do not leak data, and do not create shell/filter state.
## Cross-Cutting / Shared Pattern Reuse
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: navigation, context shell, filter chip, clear filter, browser history, global/sidebar entries, Environment Dashboard CTAs, test lane governance
- **Systems touched**: `WorkspaceHubRegistry`, `AdminSurfaceScope`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `ClearsWorkspaceHubEnvironmentFilterState`, shared chip partial, route helpers, browser tests, Feature guards
- **Existing pattern(s) to extend**: Existing Spec 314, 316, 319, 320, and 321 guard tests and browser smoke tests
- **Shared contract / presenter / builder / renderer to reuse**: Existing navigation/filter/reset helpers and existing Pest Browser conventions
- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for runtime behavior. Spec 322 may add test-only helper organization if required to avoid copying brittle browser setup across every surface.
- **Allowed deviation and why**: Test-only helper extraction is allowed when it remains under test support paths and does not become a runtime surface registry.
- **Consistency impact**: URL, visible chip, shell text, data scope, clear action, reload, and browser history must remain aligned.
- **Review focus**: Verify guards protect contracts without changing product behavior or accepting legacy aliases.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no
- **Shared OperationRun UX contract/layer reused**: N/A
- **Delegated start/completion UX behaviors**: N/A
- **Local surface-owned behavior that remains**: Existing Operations hub and OperationRun links may be used as tested surfaces only.
- **Queued DB-notification policy**: N/A
- **Terminal notification path**: N/A
- **Exception required?**: none
## Provider Boundary / Platform Core Check
- **Shared provider/platform boundary touched?**: yes, in guard assertions only
- **Boundary classification**: mixed; platform-core shell/query contracts are guarded while provider-boundary Tenant terminology remains allowlisted where it means Microsoft/Entra/provider tenant identity
- **Seams affected**: query keys, URL helpers, provider-boundary terminology allowlist, legacy Tenant route guards
- **Neutral platform terms preserved or introduced**: Workspace, Managed Environment, Environment, Provider Connection
- **Provider-specific semantics retained and why**: `tenant` remains allowed only in provider-boundary contexts documented by Spec 317 allowlist.
- **Why this does not deepen provider coupling accidentally**: The spec rejects Tenant as Environment/platform synonym and adds guards against old query aliases.
- **Follow-up path**: none unless implementation discovers a current provider-boundary ambiguity not covered by Spec 317.
## UI / Surface Guardrail Impact
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / N/A Note |
| --- | --- | --- | --- | --- | --- | --- |
| Browser regression guards for existing admin surfaces | no product UI change | Existing Filament surfaces | navigation, shell, filter chip | shell, URL-query, Livewire, table filters, session, browser history | no | Test-only guardrail |
| Coverage manifest | no | N/A | test governance | none | no | Documentation artifact for guard coverage |
## Decision-First Surface Role
Spec 322 does not add or materially change operator-facing surfaces. The guarded surfaces keep their existing roles:
- Workspace hubs remain Secondary Context / operational workspace surfaces.
- Environment-owned pages remain Environment-specific decision/diagnostic surfaces.
- Workspace-owned analysis surfaces remain workspace-level analysis surfaces.
- Configuration surfaces remain workspace configuration surfaces.
## Audience-Aware Disclosure
N/A - no product-facing detail/status surface is added or materially changed.
## UI/UX Surface Classification
No operator-facing surface classification changes are introduced. The classifications are documented in `coverage-manifest.md` for guard coverage only.
## Operator Surface Contract
No new page contract is introduced. The tests must assert that existing page contracts remain truthful:
- Workspace hub shell copy must not imply an active Environment when the URL is clean.
- Filtered workspace hubs must show visible Environment chip state.
- Environment-owned pages must show Workspace + active Environment shell context.
- Configuration surfaces must not show Environment filter chips from query aliases.
## Proportionality Review
- **New source of truth?**: no runtime source of truth; yes test-documentation truth in `coverage-manifest.md`
- **New persisted entity/table/artifact?**: no persisted entity/table; yes Spec Kit documentation artifacts
- **New abstraction?**: no runtime abstraction; possible test-only harness helpers if duplication requires them
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no runtime UI framework; the manifest uses test classifications already established by Specs 318 through 321
- **Current operator problem**: Operators and reviewers need confidence that Workspace/Environment context cannot silently drift across navigation, filters, reload, and history.
- **Existing structure is insufficient because**: Current focused tests cover slices but do not provide a durable coverage map and grouped browser guard suite across the whole stabilized chain.
- **Narrowest correct implementation**: Add explicit Feature/static guards plus focused Browser smoke files grouped by surface model, reusing existing helpers and only extracting test helpers if repeated setup becomes brittle.
- **Ownership cost**: Browser lane runtime and future maintenance when new admin surfaces are added.
- **Alternative intentionally rejected**: Full visual regression framework and broad Playwright MCP dependency.
- **Release truth**: Current-release guardrail over existing implemented contracts.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope. Legacy query aliases must not be supported or redirected.
## Testing / Lane / Runtime Impact
- **Test purpose / classification**: Feature, Heavy-Governance, Browser
- **Validation lane(s)**: fast-feedback for static/Feature guards; browser for critical shell/filter/history smoke; heavy-governance if a registry/discovery guard is added or materially expanded
- **Why this classification and these lanes are sufficient**: URL generation, classification, legacy alias rejection, and cross-workspace rejection are deterministic Feature tests. Shell/chip/reload/back-forward alignment is browser-visible and belongs in Browser lane.
- **New or expanded test families**: Expanded Spec 322 browser no-drift family plus feature guard files under `apps/platform/tests/Feature/Navigation` or existing guard directories.
- **Fixture / helper cost impact**: Existing workspace/environment factories and browser fixture patterns should be reused. Any new helper must keep workspace, membership, session, and Environment setup explicit.
- **Heavy-family visibility / justification**: Browser tests are explicitly named `Spec322...SmokeTest` and grouped by surface model to keep lane cost visible.
- **Special surface test profile**: global-context-shell
- **Standard-native relief or required special coverage**: Special coverage required because shell, URL, Livewire/table/session, and browser history can drift independently.
- **Reviewer handoff**: Reviewers must verify lane fit, helper cost, surface manifest accuracy, and that Browser tests do not become one large flaky scenario.
- **Budget / baseline / trend impact**: Browser lane expands. Runtime must be documented in implementation close-out if material.
- **Escalation needed**: document-in-feature unless the Browser lane becomes structurally expensive; then follow-up-spec.
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
- **Planned validation commands**: See `test-plan.md`.
## User Scenarios & Testing
### User Story 1 - Workspace hubs stay clean and reload-safe (Priority: P1)
As a workspace operator who previously worked inside a Managed Environment, I can open workspace hubs from sidebar/global/direct clean URLs and see Workspace-only shell, no Environment filter chip, and no legacy query state.
**Why this priority**: Clean workspace hub entry is the foundation for all other scope contracts.
**Independent Test**: Start from an Environment Dashboard with remembered Environment state, open each required workspace hub clean URL or sidebar/global link, reload, and assert clean URL, Workspace-only shell, no chip, and no legacy query params.
**Acceptance Scenarios**:
1. Given an active remembered Environment, when Operations opens through its clean workspace URL, then no Environment query parameter, shell, or chip is present.
2. Given a clean workspace hub is reloaded, when the page returns, then no stale Environment shell or chip appears.
### User Story 2 - Filtered workspace hubs expose visible and clearable Environment focus (Priority: P1)
As an operator drilling from an Environment-owned page into a filterable workspace hub, I can see the canonical `environment_id` filter as a visible chip and clear it back to the clean workspace-wide page.
**Why this priority**: Hidden filters create the highest trust risk because data can be narrowed while the shell looks workspace-wide.
**Independent Test**: Open each filterable hub with `?environment_id={id}`, assert chip and filtered data, clear it, reload, and assert workspace-wide state.
**Acceptance Scenarios**:
1. Given a valid current Workspace Environment ID, when Provider Connections opens with `environment_id`, then the shell remains Workspace-only and the chip names that Environment.
2. Given the filter is cleared, when the browser reloads, then URL, chip, table state, and shell remain clean.
### User Story 3 - Browser history never creates URL/chip/shell mismatch (Priority: P1)
As an operator using browser back and forward after filtering and clearing, I see URL, visible chip, shell context, and data scope remain aligned.
**Why this priority**: Browser history was a high-risk source of stale Livewire/session state in Specs 314 through 316.
**Independent Test**: For required high-risk hubs, open filtered URL, clear, go back, go forward, and assert alignment after each transition.
**Acceptance Scenarios**:
1. Given back returns to a URL containing `environment_id`, then the chip exists and shell remains Workspace-only.
2. Given forward returns to a clean URL, then the chip is absent and shell remains Workspace-only.
### User Story 4 - Environment-owned pages require Environment route and shell (Priority: P1)
As an operator entering an Environment-owned page, I see Workspace + Environment shell context and cannot access that page through an old clean workspace-only URL or remembered fallback.
**Why this priority**: Baseline Compare and adjacent Environment-owned pages must not regress into workspace-hub filter semantics.
**Independent Test**: Open Environment-owned pages from Environment Dashboard or canonical Environment routes, reload, navigate away/back, and assert Environment shell. Attempt former clean workspace-only access where routes exist and assert safe 404/no-access/redirect behavior.
**Acceptance Scenarios**:
1. Given Baseline Compare opens from Environment Dashboard, then its URL contains the Environment route segment and no workspace-hub `environment_id` query model.
2. Given an old workspace-only Baseline Compare URL with `environment_id`, then it does not render a valid Environment page.
### User Story 5 - Workspace-owned analysis and configuration surfaces cut away from Environment shell (Priority: P2)
As an operator opening analysis or configuration pages from an Environment origin, I see Workspace-only shell and no remembered Environment shell inheritance.
**Why this priority**: These surfaces are likely future navigation targets and must stay workspace-owned.
**Independent Test**: Start from Environment Dashboard, open each analysis/configuration surface by clean URL or navigation, reload, and assert Workspace-only shell and no chip unless the URL explicitly supports filtering.
**Acceptance Scenarios**:
1. Given Baseline Profiles opens from Environment Dashboard, then shell is Workspace-only and no Environment chip appears.
2. Given Alert Rules opens with a stray `environment_id`, then no Environment chip or shell context appears.
### User Story 6 - Legacy Tenant aliases remain invalid (Priority: P2)
As a reviewer or future implementer, I have deterministic guards proving legacy Tenant query aliases and `/admin/t` routes cannot create Environment filter or shell state.
**Why this priority**: Spec 317 cleanup must not be undone by future helper or copied test fixture drift.
**Independent Test**: Feature tests cover all legacy alias keys across representative surfaces; browser smoke covers a representative subset without making the browser suite huge.
**Acceptance Scenarios**:
1. Given `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, or `tableFilters` are present on a workspace hub URL, then no Environment filter state appears.
2. Given `/admin/t/...` is requested, then it is not an active route and no Tenant panel provider is registered.
## Edge Cases
- Cross-workspace `environment_id` is supplied to a filterable workspace hub.
- A valid current Workspace Environment ID is supplied but the actor lacks access.
- Legacy aliases appear together with unrelated query parameters.
- Legacy aliases appear together with canonical `environment_id`; only canonical behavior is allowed, and no legacy alias may survive generated links.
- Livewire table filters or deferred filters have remembered Environment-like state.
- Session-persisted table filters survive from an earlier page visit.
- Browser back/forward restores a filtered URL after clear.
- Environment-owned page has a legacy workspace-only route or landing alias.
- Alert/Audit rows with null Environment attribution exist.
- Optional pages are unreachable or lack deterministic fixtures.
- Browser profile or local environment blocks browser execution.
## Requirements
### Functional Requirements
- **FR-001**: The coverage manifest MUST list every required surface with classification, clean URL, filtered URL support, Environment route requirement, clear support, Browser coverage status, Feature coverage status, and notes.
- **FR-002**: Workspace hub clean-entry guards MUST cover Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence Overview, Reviews, Customer Reviews, Alert Deliveries, and Audit Log.
- **FR-003**: Filtered workspace hub guards MUST cover filterable hubs using canonical `environment_id` and MUST reject legacy query aliases.
- **FR-004**: Clear-filter guards MUST prove URL, Livewire state, Filament table filters, deferred filters, persisted/session state, chip state, and shell context return to clean workspace-wide state where the surface supports clearing.
- **FR-005**: Browser back/forward guards MUST cover Provider Connections, Finding Exceptions Queue, Customer Reviews, Evidence Overview, Alert Deliveries, and Audit Log.
- **FR-006**: Environment-owned page guards MUST cover Environment Dashboard, Baseline Compare, Required Permissions, Provider Readiness / Onboarding Readiness, and Inventory or Inventory Coverage if browser-stable.
- **FR-007**: Baseline Compare MUST have a browser guard and a clean workspace-only invalid-access guard.
- **FR-008**: Workspace-owned analysis guards MUST cover Baseline Profiles, Baseline Snapshots, My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare.
- **FR-009**: Workspace configuration guards MUST cover Alert Rules, Alert Destinations, and Workspace Settings as Workspace-only surfaces.
- **FR-010**: Legacy Tenant alias guards MUST cover `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- **FR-011**: No active `/admin/t` route and no active `TenantPanelProvider` may exist.
- **FR-012**: Provider-boundary Tenant terminology MUST remain allowed only through the Spec 317 allowlist or equivalent current guard.
- **FR-013**: Cross-workspace Environment IDs MUST be rejected with safe no-access behavior and must not leak data or switch Workspace.
- **FR-014**: Browser helpers MUST avoid brittle selectors where stable text, route, ARIA, or `data-testid` hooks already exist.
- **FR-015**: Browser tests MUST stay grouped and focused rather than one large all-in-one scenario.
- **FR-016**: Any browser gap MUST be documented in the coverage manifest with a blocker reason and backed by deterministic Feature/Unit coverage.
- **FR-017**: No product behavior may be changed except narrow fixes required to satisfy existing Specs 314 through 321 contracts.
- **FR-018**: No migrations, seeders, packages, env vars, queues, scheduler, storage, or deployment asset changes are allowed unless the spec is updated before implementation continues.
### Non-Functional Requirements
- **NFR-001**: Browser tests MUST avoid sleeps and volatile row-count assertions unless seeded data makes them deterministic.
- **NFR-002**: Feature/static guards SHOULD cover exhaustive alias and registry checks so Browser tests remain smoke-level.
- **NFR-003**: Test setup MUST keep workspace, membership, Environment access, session, and provider fixture cost explicit.
- **NFR-004**: Test names MUST make heavy/browser lane cost visible, using `Spec322` prefixes.
- **NFR-005**: Filament v5 / Livewire v4 patterns MUST remain in test code. No Livewire v3 or Filament v3/v4 assumptions.
- **NFR-006**: Browser screenshots MAY be diagnostic, but assertions are authoritative.
- **NFR-007**: Guard failures MUST identify the surface and mismatch clearly enough for follow-up repair.
- **NFR-008**: The implementation MUST not introduce Playwright MCP dependency; use Pest Browser coverage unless the spec is explicitly revised.
## Data / Truth-Source Requirements
- **DTR-001**: The coverage manifest is the documentation truth for Spec 322 guard coverage, not runtime product truth.
- **DTR-002**: Test data must use existing factories and persisted records only to prove scope alignment.
- **DTR-003**: Environment filtering truth remains canonical `environment_id`; legacy aliases are invalid inputs.
- **DTR-004**: Provider Tenant terms remain provider-boundary terminology only.
## Out Of Scope
- New product features.
- Redesigning pages.
- Changing navigation IA unless a guard reveals an existing in-scope regression requiring a narrow fix.
- Changing Environment CTA behavior except to satisfy existing contract.
- Adding new Environment filters.
- Changing Alerts/Audit Log decisions from Spec 321.
- Refactoring all Filament pages.
- Adding a full visual-regression framework.
- Introducing Playwright MCP dependency.
- Requiring manual screenshots for every run.
- Adding flaky tests.
- Adding migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, or compatibility layers.
## Success Criteria
- **SC-001**: `coverage-manifest.md` exists and lists all required surfaces with coverage status and gaps.
- **SC-002**: Feature/static guards cover classification, clean URLs, Environment CTA URLs, clear filter, cross-workspace rejection, legacy aliases, `/admin/t`, Tenant panel provider absence, and provider Tenant allowlist.
- **SC-003**: Browser guards cover clean workspace hub entry, filtered entry, clear/reload, back/forward, Environment-owned pages, workspace-owned analysis pages, workspace configuration pages, and Alerts/Audit Log contracts at the scope defined in `test-plan.md`.
- **SC-004**: Browser gaps, if any, are explicit and have deterministic non-browser guard coverage.
- **SC-005**: Targeted Feature/Unit and Browser commands pass, or failures are documented with exact blockers.
- **SC-006**: `git diff --check` passes.
- **SC-007**: No broad runtime/product change is introduced.
## Assumptions
- Pest Browser is the preferred durable browser lane because the repo already uses it.
- Existing browser helpers and fixtures can be reused or lightly factored without adding runtime abstraction.
- Spec 321 runtime implementation is present in current branch history and may be guarded by Spec 322.
- Some optional surfaces may remain Feature-covered only if browser fixtures are not stable or routes are not reachable.
## Open Questions
None blocking preparation.
Implementation may discover browser-only fixture gaps. Those should be documented in `coverage-manifest.md` and covered by deterministic Feature/Unit guards rather than silently skipped.
## Follow-Up Spec Candidates
- Customer Review Workspace v1 polish / productization.
- Decision-Based Governance Inbox v1 polish.
- Localization v1 customer-facing surfaces.
- Cross-Tenant Compare & Promotion v1 productization.
- Commercial entitlements / billing-state maturity.
These are product lanes and are not part of Spec 322.
## Required Final Report Shape
Implementation close-out must report:
- Changed behavior.
- Coverage added.
- Feature tests command and result.
- Browser tests command and result.
- Coverage manifest status.
- Known browser gaps.
- Remaining risks.
- Test files added/updated.
- Surfaces covered.
- Surfaces excluded with reason.
- Whether screenshots were saved.
- Whether full suite was run.
- Known unrelated residual failures.
- Confirmation that no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment asset changes, backwards compatibility layer, or legacy query alias support were added.

View File

@ -0,0 +1,217 @@
# Tasks: Browser No-Drift Regression Guard
**Input**: Spec artifacts from `specs/322-browser-no-drift-regression-guard/`
**Prerequisites**: `spec.md`, `plan.md`, `test-plan.md`, `coverage-manifest.md`
**Runtime posture**: Test/guard-first. No new product behavior unless a narrow fix is required to satisfy existing Specs 314 through 321 contracts.
## Phase 1: Discovery Confirmation
- [x] T001 Re-read `specs/322-browser-no-drift-regression-guard/spec.md`, `plan.md`, `test-plan.md`, and `coverage-manifest.md` before implementation starts.
- [x] T002 Re-read Specs 314 through 321 as historical/context dependencies without editing their completed artifacts.
- [x] T003 Inspect `apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php` and confirm required workspace hubs and exclusions still match the manifest.
- [x] T004 Inspect `apps/platform/app/Support/Navigation/AdminSurfaceScope.php` and confirm workspace-owned analysis surface classification still matches the manifest.
- [x] T005 Inspect `apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php` and confirm canonical `environment_id` remains the only accepted filter query key.
- [x] T006 Inspect `apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php` and confirm legacy Environment-like query/table/session keys remain resettable.
- [x] T007 Inspect route names and URLs for all required surfaces listed in `specs/322-browser-no-drift-regression-guard/coverage-manifest.md`.
- [x] T008 Update `specs/322-browser-no-drift-regression-guard/coverage-manifest.md` if discovery proves a route, classification, or existing coverage status has changed.
## Phase 2: Feature / Static Guards
- [x] T009 [P] Add `it('classifies_core_admin_surfaces_without_scope_drift')` in `apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php`.
- [x] T010 [P] Add `it('workspace_hub_clean_urls_never_emit_environment_or_legacy_query_params')` in `apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php`.
- [x] T011 [P] Add `it('clear_filter_results_match_clean_workspace_hub_entry_for_filterable_hubs')` in `apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php`.
- [x] T012 [P] Add `it('environment_id_filters_reject_cross_workspace_environment_ids')` in `apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php`.
- [x] T013 [P] Add `it('environment_cta_urls_use_the_correct_surface_contract')` in `apps/platform/tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php`.
- [x] T014 [P] Add `it('legacy_environment_query_aliases_do_not_create_filter_or_shell_state')` in `apps/platform/tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php`.
- [x] T015 [P] Add `it('has_no_active_legacy_tenant_panel_routes')` coverage by extending or referencing `apps/platform/tests/Feature/Guards/NoLegacyTenantPanelRuntimeTest.php`.
- [x] T016 [P] Add `it('allows_tenant_terms_only_in_provider_boundary_contexts')` coverage by extending `apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php` or adding a narrow Spec 322 guard.
- [x] T017 Confirm Feature/static guards cover Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence Overview, Reviews, Customer Reviews, Alert Deliveries, Audit Log, Alerts, Alert Rules, Alert Destinations, Workspace Settings, Baseline Profiles, Baseline Snapshots, My Findings, Findings Intake, Findings Hygiene, Cross-environment Compare, and Baseline Compare.
## Phase 3: Browser Harness And Fixture Control
- [x] T018 Inspect existing Pest Browser patterns in `apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php` and `apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php`.
- [x] T019 Decide whether a test-only helper such as `apps/platform/tests/Browser/Support/Spec322WorkspaceEnvironmentBrowserHarness.php` is necessary, or keep helpers local to the Spec 322 browser files.
- [x] T020 If a harness is added, keep workspace, Environment, membership, and session setup explicit and opt-in in `apps/platform/tests/Browser/Support/Spec322WorkspaceEnvironmentBrowserHarness.php`.
- [x] T021 If a harness is added, document its fixture-cost boundary in `specs/322-browser-no-drift-regression-guard/coverage-manifest.md`.
- [x] T022 Avoid adding seeders; if deterministic browser fixtures cannot be built with factories, stop and update `spec.md` and `plan.md` before changing seeders.
## Phase 4: Workspace Hub Browser Guards
- [x] T023 [P] Add `apps/platform/tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php` covering clean workspace hub entry from Environment origin.
- [x] T024 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Operations.
- [x] T025 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Governance Inbox.
- [ ] T026 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Decision Register.
- Covered by existing Spec314/316 browser anchors plus Spec322 Feature/static guards; not duplicated in the focused Spec322 browser smoke.
- [ ] T027 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Finding Exceptions Queue.
- Covered by existing Spec314/316 browser anchors plus Spec322 Feature/static guards; Spec322 browser includes this surface in the representative legacy alias guard.
- [x] T028 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Provider Connections.
- [x] T029 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Evidence Overview.
- [ ] T030 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Reviews and Customer Reviews.
- Covered by existing Spec314/316 browser anchors plus Spec322 Feature/static guards; not duplicated in the focused Spec322 browser smoke.
- [ ] T031 [P] In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover clean URL, Workspace-only shell, no Environment chip, no legacy params, and reload for Alert Deliveries and Audit Log.
- Covered in `Spec322AlertsAuditNoDriftSmokeTest.php`, with Audit Log clear/history retained as Feature fallback due browser click timing.
- [ ] T032 In `Spec322WorkspaceHubNoDriftSmokeTest.php`, cover filtered entry, visible chip, clear, and reload for all filterable workspace hubs; include browser back/forward alignment for Provider Connections, Finding Exceptions Queue, Customer Reviews, Evidence Overview, Alert Deliveries, and Audit Log.
- Spec322 browser covers Provider Connections, Evidence Overview, and Alert Deliveries; existing Spec316 browser and Spec322 Feature/static guards cover the remaining permutations.
## Phase 5: Environment-Owned Browser Guards
- [x] T033 Add `apps/platform/tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php` covering Environment-owned route/shell contracts.
- [ ] T034 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Environment Dashboard route, Workspace + Environment shell, reload, and back from a workspace hub.
- Spec322 browser covers route/shell/reload; browser back is left to existing navigation anchors to keep this guard bounded.
- [ ] T035 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Baseline Compare route, Workspace + Environment shell, no workspace-hub `environment_id` model, reload, and browser back.
- Spec322 browser covers route/shell/reload and invalid old access; browser back is left to existing navigation anchors to keep this guard bounded.
- [x] T036 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Baseline Compare old clean workspace-only invalid access using the existing `BaselineCompareEnvironmentRouteContractTest` behavior as the deterministic baseline.
- [x] T037 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Required Permissions if route and fixture are browser-stable; otherwise document browser gap and Feature fallback in `coverage-manifest.md`.
- [ ] T038 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Provider Readiness / Onboarding Readiness if route and fixture are browser-stable; otherwise document browser gap and Feature fallback in `coverage-manifest.md`.
- No separate route was confirmed; coverage manifest documents Environment Dashboard browser coverage as the deterministic fallback.
- [ ] T039 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Inventory or Inventory Coverage if route and fixture are browser-stable; otherwise document browser gap and Feature fallback in `coverage-manifest.md`.
- Browser coverage intentionally excluded for runtime cost; Feature URL/scope guards cover the route contract.
- [ ] T040 In `Spec322EnvironmentOwnedSurfaceSmokeTest.php`, cover Environment Diagnostics if route and fixture are browser-stable; otherwise document browser gap and Feature fallback in `coverage-manifest.md`.
- Browser coverage intentionally excluded for runtime cost; Feature URL/scope guards cover the route contract.
## Phase 6: Workspace-Owned Analysis And Configuration Browser Guards
- [x] T041 Add `apps/platform/tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php` covering Workspace-only shell cutover from Environment origin.
- [x] T042 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Baseline Profiles clean URL, Workspace-only shell, no Environment chip, and reload.
- [ ] T043 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Baseline Snapshots clean URL, Workspace-only shell, no Environment chip, and reload.
- Browser coverage intentionally excluded for runtime cost; Spec322 Feature/static guards cover the surface classification and URL contract.
- [x] T044 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover My Findings clean URL, Workspace-only shell, no Environment chip, and reload.
- [ ] T045 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Findings Intake clean URL, Workspace-only shell, no Environment chip, and reload.
- Browser coverage intentionally excluded for runtime cost; Spec322 Feature/static guards cover the surface classification and URL contract.
- [ ] T046 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Findings Hygiene clean URL, Workspace-only shell, no Environment chip, and reload.
- Browser coverage intentionally excluded for runtime cost; Spec322 Feature/static guards cover the surface classification and URL contract.
- [x] T047 [P] In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Cross-environment Compare clean URL, Workspace-only shell, no Environment chip, and reload.
- [ ] T048 In `Spec322WorkspaceOwnedAnalysisSmokeTest.php`, cover Alert Rules, Alert Destinations, and Workspace Settings as Workspace-only configuration surfaces from Environment origin.
- Workspace Settings is covered here; Alert Rules and Alert Destinations are covered in `Spec322AlertsAuditNoDriftSmokeTest.php`.
## Phase 7: Alerts / Audit Log Browser Guards
- [x] T049 Add `apps/platform/tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php`.
- [ ] T050 In `Spec322AlertsAuditNoDriftSmokeTest.php`, cover Alerts Overview clean and filtered entry, visible chip, clear, reload, and links to Alert Deliveries/Audit Log.
- Spec322 browser covers clean and filtered entry; clear/link permutations remain covered by Spec321/Spec322 Feature guards.
- [x] T051 In `Spec322AlertsAuditNoDriftSmokeTest.php`, cover Alert Deliveries filtered entry, visible chip, clear, reload, and back/forward alignment.
- [ ] T052 In `Spec322AlertsAuditNoDriftSmokeTest.php`, cover Audit Log filtered entry, visible chip, clear, reload, and back/forward alignment.
- Spec322 browser covers filtered and clean/reload shell state; clear/back-forward remains covered by Spec321/Spec322 Feature guards because browser click timing was unstable on this page.
- [x] T053 In `Spec322AlertsAuditNoDriftSmokeTest.php`, cover Alert Rules and Alert Destinations reject stray `environment_id` chip/shell behavior in the browser.
- [x] T054 Ensure `Spec322AlertsAuditNoDriftSmokeTest.php` aligns with `apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php` and does not change Spec 321 product decisions.
## Phase 8: Legacy Alias Browser Representative Guard
- [x] T055 Add representative browser coverage for legacy aliases in `apps/platform/tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php` or `apps/platform/tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php`.
- [x] T056 Keep exhaustive alias permutations in Feature tests rather than Browser tests.
- [x] T057 Assert representative generated links do not preserve `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, or `tableFilters`.
## Phase 9: Manifest, Screenshots, And Documentation Artifacts
- [x] T058 Update `specs/322-browser-no-drift-regression-guard/coverage-manifest.md` with actual Browser and Feature coverage statuses after tests are implemented.
- [x] T059 Mark any browser blocker or intentionally excluded optional surface in `specs/322-browser-no-drift-regression-guard/coverage-manifest.md`.
- [x] T060 Save diagnostic screenshots under `specs/322-browser-no-drift-regression-guard/artifacts/screenshots/` only when useful for debugging or final review.
- No final-review screenshots were needed; transient failed-run screenshots remained in the standard Pest Browser screenshot directory only.
- [x] T061 Do not add implementation documentation outside `specs/322-browser-no-drift-regression-guard/` unless the user explicitly requests it.
## Phase 10: Validation
- [x] T062 Run targeted Feature/static guards from `specs/322-browser-no-drift-regression-guard/test-plan.md`.
- [x] T063 Run targeted Browser guards from `specs/322-browser-no-drift-regression-guard/test-plan.md`.
- [ ] T064 Run existing related regression filter command from `specs/322-browser-no-drift-regression-guard/test-plan.md`.
- The original combined command was attempted and then split for reviewability. The Unit/Feature slice completes with the Baseline Compare failures documented below. The Spec322 Browser slice completes green. The original combined command is not used as the merge gate because the mixed Feature/Browser run previously stalled behind a Playwright server after reporting Feature failures.
- [x] T065 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
- [x] T066 Run `git diff --check` from the repository root.
- [x] T067 Record exact commands, results, browser gaps, screenshots, full-suite status, and unrelated residual failures in the final implementation report.
### Broader Regression Note
Scope check:
- Branch: `322-browser-no-drift-regression-guard`
- Diff scope: only Spec 322 spec artifacts, Spec 322 Feature tests, Spec 322 Browser tests, and `tests/Browser/Support/Spec322WorkspaceEnvironmentBrowserHarness.php`.
- No 319/320/321 artifacts are included in the current diff.
Targeted Spec 322 gates:
- `./vendor/bin/sail artisan test tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php --compact`
- Result: 8 passed, 400 assertions.
- `./vendor/bin/sail artisan test tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php --compact`
- Result: 9 passed, 510 assertions.
Broader regression split:
- `./vendor/bin/sail artisan test tests/Unit tests/Feature --filter='WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|BaselineCompare|WorkspaceOwnedAnalysis|AlertsAudit' --compact --stop-on-failure`
- Result: failed at the first Baseline Compare explanation failure before any Browser tests were involved.
- `./vendor/bin/sail artisan test tests/Unit tests/Feature --filter='WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|BaselineCompare|WorkspaceOwnedAnalysis|AlertsAudit' --compact`
- Result: 5 failed, 237 passed, 1884 assertions.
Known remaining broader-regression failures:
- `Tests\Feature\Baselines\BaselineCompareExplanationFallbackTest`
- Test: `it shows an unavailable explanation before any baseline compare result exists`
- Individual rerun: fails.
- Failure: expected page output to contain `A current baseline compare result is not available yet.`
- `Tests\Feature\Filament\BaselineCompareExplanationSurfaceTest`
- Test: `it renders suppressed baseline-compare results as explanation-first guidance`
- Individual rerun: fails.
- Failure: expected page output to contain `The last compare finished, but normal result output was suppressed.`
- `Tests\Feature\Filament\BaselineCompareSummaryConsistencyTest`
- Test: `it keeps widget, landing, and banner equally cautious for the same limited-confidence result`
- Individual rerun: fails.
- Failure: expected page output to contain `Needs review`.
- `Tests\Feature\Filament\BaselineCompareSummaryConsistencyTest`
- Test: `it keeps widget, landing, and banner aligned when overdue workflow makes the state action-required`
- Individual rerun: fails.
- Failure: expected page output to contain `Action required`.
- `Tests\Feature\Filament\BaselineCompareSummaryConsistencyTest`
- Test: `it keeps widget, landing, and banner aligned while compare is still in progress`
- Individual rerun: fails.
- Failure: expected page output to contain `In progress`.
Spec322-caused broader-regression issue found and fixed:
- `Tests\Feature\Guards\LegacyTenantPlatformContextCleanupTest` initially failed because `Spec322LegacyQueryAliasGuardTest.php` contained the retired literal `ensure-filament-tenant-selected` inside its own search pattern list.
- Fixed by splitting the literal in the pattern construction while preserving the generated regex.
- Individual rerun result: `LegacyTenantPlatformContextCleanupTest` passes, 6 tests, 19 assertions.
Out-of-scope assessment:
- The remaining 5 failures are in untouched Baseline Compare test files and reproduce when run individually.
- They are not Browser-harness failures and not Spec322 targeted guard failures.
- They are not proven pre-existing against a clean `dev` checkout in this working tree, but they are independent of the Spec322 diff: Spec322 changed only tests/spec artifacts and no Baseline Compare product code.
- Treat these as a separate Baseline Compare test-suite hygiene/runtime investigation, not as a Spec322 no-drift regression.
## Phase 11: Non-Goals / Stop Conditions
- [x] NT001 Do not add migrations.
- [x] NT002 Do not change seeders unless `spec.md` and `plan.md` are updated first with fixture justification.
- [x] NT003 Do not add packages, env vars, queues, scheduler, storage, or deployment asset changes.
- [x] NT004 Do not introduce Playwright MCP dependency or a broad visual-regression framework.
- [x] NT005 Do not add backwards compatibility redirects, dual query contracts, or legacy query alias support.
- [x] NT006 Do not rewrite completed Specs 314 through 321 or remove their close-out and validation history.
- [x] NT007 Do not change Alerts/Audit Log decisions from Spec 321.
- [x] NT008 Stop and update `spec.md` and `plan.md` before any product behavior change broader than a narrow contract fix.
## Dependencies
1. Phase 1 must complete before Feature/static or Browser implementation.
2. Feature/static guards in Phase 2 can run in parallel by file.
3. Browser harness decision in Phase 3 should happen before Phases 4 through 8.
4. Browser surface phases can run in parallel if they write separate files.
5. Manifest updates in Phase 9 depend on actual implementation results.
6. Validation in Phase 10 depends on all in-scope test files being in place.
## MVP Scope
The minimum viable Spec 322 implementation is:
1. Feature/static guards for classification, clean URLs, CTA URLs, clear-state equivalence, cross-workspace rejection, legacy aliases, `/admin/t`, and provider Tenant allowlist.
2. Browser smoke for workspace hubs, Baseline Compare / Environment-owned route, workspace-owned analysis shell cutover, and Alerts/Audit Log.
3. Updated coverage manifest with any browser gaps.
## Parallel Work Examples
```text
Agent A: Feature/static guards in apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php
Agent B: Legacy alias and provider-boundary guards in apps/platform/tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php
Agent C: Workspace hub browser smoke in apps/platform/tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php
Agent D: Environment-owned browser smoke in apps/platform/tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php
```
Parallel workers must not edit the same test file at the same time.

View File

@ -0,0 +1,139 @@
# Spec 322 Test Plan
## Purpose
Prove the Workspace / Environment context contracts from Specs 314 through 321 remain durable across Feature/static guards and Pest Browser smoke tests.
Assertions are authoritative. Screenshots are diagnostic only.
## Test Families
### Feature / Static Guards
Suggested files:
```text
apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php
apps/platform/tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php
apps/platform/tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php
```
Required assertions:
- Core surfaces classify as `workspace_hub`, `environment_owned_page`, `workspace_owned_analysis_surface`, or `workspace_configuration_surface`.
- Clean workspace hub URLs never emit `environment_id`, `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, or `tableFilters`.
- Environment CTAs use canonical `environment_id` for filterable workspace hubs and Environment routes for Environment-owned pages.
- Clear-filter behavior matches clean workspace hub entry.
- Cross-workspace `environment_id` filters reject safely.
- Legacy Environment/Tenant query aliases do not create filter or shell state.
- `/admin/t` routes and `TenantPanelProvider` remain absent.
- Provider Tenant terms remain allowed only in provider-boundary allowlist contexts.
### Browser Contract Guards
Suggested files:
```text
apps/platform/tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php
apps/platform/tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php
apps/platform/tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php
apps/platform/tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php
```
Required assertions:
- Workspace hub clean sidebar/global/direct entry from Environment origin remains clean and Workspace-only.
- Filterable workspace hubs opened with `environment_id` show visible Environment chip, keep Workspace-only shell, and reject legacy params.
- Clear filter removes query/chip/table/session state and remains clean after reload.
- Browser back/forward keeps URL, chip, shell, and data aligned.
- Environment-owned pages require Environment route and show Environment shell after reload/back.
- Baseline Compare rejects old clean workspace-only access.
- Workspace-owned analysis surfaces cut to Workspace-only shell from Environment origin.
- Workspace configuration surfaces remain Workspace-only and chip-free.
- Alerts/Audit Log Spec 321 decisions are protected in browser.
## Focused Commands
Run targeted Feature/static gates:
```bash
cd apps/platform
./vendor/bin/sail artisan test \
tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php \
tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php \
tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php \
--compact
```
Run targeted Browser gates:
```bash
cd apps/platform
./vendor/bin/sail artisan test \
tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php \
tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php \
tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php \
tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php \
--compact
```
Run existing related regression gates:
```bash
cd apps/platform
./vendor/bin/sail artisan test --filter='WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|BaselineCompare|WorkspaceOwnedAnalysis|AlertsAudit' --compact
```
Run final formatting and diff checks:
```bash
cd apps/platform
./vendor/bin/sail pint --dirty
git diff --check
```
## Existing Anchors To Reuse
```text
apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php
apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php
apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php
apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php
apps/platform/tests/Feature/Guards/NoLegacyTenantPanelRuntimeTest.php
apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php
apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php
```
Spec 322 should extend these anchors where that is narrower than adding duplicate tests.
## Browser Fixture Rules
- Use explicit workspace, Managed Environment, membership, and session setup.
- Seed only records required to prove data scope.
- Avoid global seed assumptions.
- Avoid sleeps.
- Prefer stable text, route, ARIA, or existing `data-testid` selectors.
- Avoid volatile counters unless fixture records make the count deterministic.
- Keep each browser file focused by surface model.
## Screenshot Policy
Screenshots may be saved under:
```text
specs/322-browser-no-drift-regression-guard/artifacts/screenshots/
```
Screenshot capture is diagnostic. A failed screenshot capture must not replace browser assertions. A browser blocker must be recorded in `coverage-manifest.md`.
## Full Suite Statement
Do not claim full-suite green unless the full suite was run.
If only targeted commands are run, the final report must say targeted gates passed and full suite was not run.