## Summary - add the baseline compare landing experience for the environment dashboard productization flow - expand the environment dashboard overview and summary-building logic to support richer baseline comparison states and assessments - update the supporting Blade templates for the new compare and overview presentation - add English and German translations for the baseline compare surface - include the Spec 330 planning and task artifacts alongside the implementation ## Tests - touched browser, feature, and unit coverage for the new baseline compare flow - updated test files include `Spec330EnvironmentDashboardBaselineCompareSmokeTest`, `BaselineCompareLandingWhyNoFindingsTest`, `Spec330EnvironmentDashboardBaselineCompareProductizationTest`, `HeaderContextBarTest`, and `ManagedEnvironmentModelTest` - no additional test run was performed as part of this commit/push/PR workflow Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #392
326 lines
16 KiB
PHP
326 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\BaselineCompareLanding;
|
|
use App\Filament\Widgets\Dashboard\EnvironmentDashboardOverview;
|
|
use App\Models\Finding;
|
|
use App\Models\OperationRun;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Livewire\Livewire;
|
|
|
|
it('keeps the Spec 330 repo truth map present with the required source areas', function (): void {
|
|
$path = collect([
|
|
dirname(base_path(), 2).'/specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md',
|
|
dirname(base_path()).'/repo/specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md',
|
|
])->first(fn (string $candidate): bool => is_file($candidate));
|
|
|
|
expect($path)->not->toBeNull();
|
|
|
|
$contents = file_get_contents($path);
|
|
|
|
expect($contents)
|
|
->toContain('Environment Dashboard')
|
|
->toContain('Baseline Compare')
|
|
->toContain('Provider connection/readiness')
|
|
->toContain('Required permissions')
|
|
->toContain('Backup sets')
|
|
->toContain('Restore runs')
|
|
->toContain('Baseline profile assignment')
|
|
->toContain('Baseline compare result')
|
|
->toContain('OperationRuns');
|
|
});
|
|
|
|
it('renders the Environment Dashboard as a decision-first readiness workbench', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
foreach (range(1, 3) as $index) {
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subHours($index),
|
|
]);
|
|
}
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = Livewire::test(EnvironmentDashboardOverview::class)
|
|
->assertSee('Is this environment ready, blocked, stale, or requiring review?')
|
|
->assertSee('Status')
|
|
->assertSee('Reason')
|
|
->assertSee('Impact')
|
|
->assertSee('Readiness proof')
|
|
->assertSee('Next action')
|
|
->assertSee('Readiness dimensions')
|
|
->assertSee('Recommended next actions')
|
|
->assertSee('Supporting signals')
|
|
->assertSee('Additional readiness signals used to explain the current recommendation.')
|
|
->assertSee('Signal')
|
|
->assertSee('State')
|
|
->assertSee('Action')
|
|
->assertSee('Unavailable')
|
|
->assertSee('Not ready')
|
|
->assertSee('Absent')
|
|
->assertSee('No active review')
|
|
->assertSee('No customer-safe output')
|
|
->assertSee('Baseline missing')
|
|
->assertSee('Operations follow-up')
|
|
->assertSee('3 require review')
|
|
->assertSee('Open operations hub')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('Secondary context')
|
|
->assertDontSee('Show operation details')
|
|
->assertDontSee('View operation details')
|
|
->assertDontSee('Ab...')
|
|
->assertDontSee('Unava...')
|
|
->assertDontSee('Not re...')
|
|
->assertDontSee('No customer-saf...')
|
|
->assertDontSee('Baseline assig...')
|
|
->assertDontSee('Fully ready')
|
|
->assertDontSee('Protected')
|
|
->assertDontSee('Compliant')
|
|
->assertDontSee('raw payload')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('provider secret')
|
|
->assertDontSee('stack trace')
|
|
->assertDontSee('debug metadata')
|
|
->assertDontSee('internal exception')
|
|
->assertDontSee('provider response');
|
|
|
|
$content = $component->html();
|
|
$readinessDecisionPosition = strpos($content, 'data-testid="tenant-dashboard-readiness-decision"');
|
|
$supportingSignalsPosition = strpos($content, 'data-testid="tenant-dashboard-supporting-signals"');
|
|
$operationFollowUpPosition = strpos($content, 'data-signal-key="operations_follow_up"');
|
|
$diagnosticsPosition = strpos($content, 'data-testid="tenant-dashboard-diagnostics"');
|
|
|
|
expect(substr_count($content, 'data-testid="tenant-dashboard-readiness-decision"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-proof-panel"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-card"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-supporting-signals"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-supporting-signal"'))->toBe(6)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-operations-attention-summary"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-operations-attention-item"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-operation-details"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-diagnostics"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="tenant-dashboard-status-badge"'))->toBeGreaterThan(0)
|
|
->and($content)->not->toContain('tenant-dashboard-secondary-context')
|
|
->and($content)->not->toContain('data-testid="tenant-dashboard-diagnostics" open')
|
|
->and($content)->not->toContain('truncate')
|
|
->and($content)->not->toContain('text-overflow')
|
|
->and($readinessDecisionPosition)->not->toBeFalse()
|
|
->and($supportingSignalsPosition)->not->toBeFalse()
|
|
->and($operationFollowUpPosition)->not->toBeFalse()
|
|
->and($diagnosticsPosition)->not->toBeFalse()
|
|
->and($supportingSignalsPosition)->toBeGreaterThan($readinessDecisionPosition)
|
|
->and($operationFollowUpPosition)->toBeGreaterThan($supportingSignalsPosition)
|
|
->and($diagnosticsPosition)->toBeGreaterThan($supportingSignalsPosition);
|
|
});
|
|
|
|
it('keeps dynamic Tenant display names while avoiding static tenant copy on the dashboard', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$environment->forceFill(['name' => 'Spec330 Tenant Named Environment'])->save();
|
|
|
|
$this->actingAs($user);
|
|
|
|
$this->get(ManagedEnvironmentLinks::viewUrl($environment))
|
|
->assertOk()
|
|
->assertSeeText('Spec330 Tenant Named Environment')
|
|
->assertDontSeeText('MANAGED_ENVIRONMENT')
|
|
->assertDontSeeText('current tenant')
|
|
->assertDontSeeText('tenant filter')
|
|
->assertDontSeeText('all tenants')
|
|
->assertDontSeeText('choose tenant')
|
|
->assertDontSeeText('tenant scope');
|
|
});
|
|
|
|
it('renders Baseline Compare no-assignment as an actionable unavailable decision state', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Which baseline drift requires action?')
|
|
->assertSee('Baseline not assigned')
|
|
->assertSee('Baseline compare cannot be used for governance decisions until an assignment exists.')
|
|
->assertSee('Compare trust is unavailable until a baseline assignment exists.')
|
|
->assertSee('No usable drift result is available yet.')
|
|
->assertSee('Open baseline profiles to assign a baseline to this environment.')
|
|
->assertSee('Evidence path')
|
|
->assertSee('Next action')
|
|
->assertSee('Compare readiness flow')
|
|
->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.')
|
|
->assertSee('Baseline assigned')
|
|
->assertSee('Missing')
|
|
->assertSee('No baseline is assigned to this environment.')
|
|
->assertSee('Baseline snapshot')
|
|
->assertSee('No baseline snapshot is linked.')
|
|
->assertSee('Environment snapshot')
|
|
->assertSee('Current environment evidence is present.')
|
|
->assertSee('Environment snapshot state is required for compare.')
|
|
->assertSee('Compare run')
|
|
->assertSee('Compare cannot run until required inputs exist.')
|
|
->assertSee('Decision output')
|
|
->assertSee('No drift decision output is available yet.')
|
|
->assertSee('Available inputs')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Unavailable because no baseline assigned.')
|
|
->assertSee('What this unlocks after assignment')
|
|
->assertSee('Actionable drift categories')
|
|
->assertSee('Evidence-backed compare')
|
|
->assertSee('Governance decision path')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('No Baseline Assigned')
|
|
->assertDontSee('This environment does not have an assigned baseline yet.')
|
|
->assertDontSee('No coverage warning is currently reported for the latest compare.')
|
|
->assertDontSee('Readiness overview')
|
|
->assertDontSee('Recent baseline activity')
|
|
->assertDontSee('0% Ready')
|
|
->assertDontSee('Assigned baseline')
|
|
->assertDontSee('Drift impact')
|
|
->assertDontSee('Evidence gap details')
|
|
->assertDontSee('Search recorded gap subjects')
|
|
->assertDontSee('raw payload')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('provider secret')
|
|
->assertDontSee('stack trace')
|
|
->assertDontSee('debug metadata')
|
|
->assertDontSee('internal exception')
|
|
->assertDontSee('provider response');
|
|
|
|
$content = $component->html();
|
|
$visibleLabelCount = static fn (string $label): int => preg_match_all('/>\s*'.preg_quote($label, '/').'\s*</', $content);
|
|
|
|
expect(substr_count($content, 'data-testid="baseline-compare-decision-workbench"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-decision-summary"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-proof-item"'))->toBe(0)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-readiness-flow"'))->toBe(1)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-readiness-step"'))->toBe(5)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-readiness-connector"'))->toBe(4)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(3)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-assignment-unlocks"'))->toBe(1)
|
|
->and($content)->toContain('aria-label="Compare readiness pipeline"')
|
|
->and($content)->toContain('data-connector-label="Baseline assigned to Baseline snapshot"')
|
|
->and($content)->toContain('data-connector-label="Baseline snapshot to Environment snapshot"')
|
|
->and($content)->toContain('data-connector-label="Environment snapshot to Compare run"')
|
|
->and($content)->toContain('data-connector-label="Compare run to Decision output"')
|
|
->and($content)->toMatch('/data-step-label="Baseline assigned"[\s\S]*?data-step-state="Missing"[\s\S]*?data-step-current-blocker="true"[\s\S]*?>\s*Missing\s*</')
|
|
->and($content)->toMatch('/data-step-label="Environment snapshot"[\s\S]*?>\s*Available\s*</')
|
|
->and($content)->toMatch('/data-step-label="Compare run"[\s\S]*?>\s*Unavailable\s*</')
|
|
->and($content)->toMatch('/data-input-label="Environment snapshot"[\s\S]*?>\s*Available\s*</')
|
|
->and($content)->toMatch('/data-input-label="Operation proof"[\s\S]*?>\s*Unavailable\s*</')
|
|
->and($visibleLabelCount('Assigned baseline'))->toBe(0)
|
|
->and($visibleLabelCount('Compare trust'))->toBe(0)
|
|
->and($visibleLabelCount('Drift impact'))->toBe(0)
|
|
->and($visibleLabelCount('Evidence path'))->toBe(1)
|
|
->and($content)->not->toContain('data-testid="baseline-compare-diagnostics" open')
|
|
->and($content)->not->toContain('evidence_gap_details')
|
|
->and($content)->not->toContain('raw diff');
|
|
});
|
|
|
|
it('renders Baseline Compare drift and evidence summary before support diagnostics', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
|
|
|
$run = seedBaselineCompareRun($environment, $profile, $snapshot, [
|
|
'reason_code' => \App\Support\Baselines\BaselineCompareReasonCode::CoverageUnproven->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration', 'deviceCompliancePolicy'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => ['deviceCompliancePolicy'],
|
|
'proof' => true,
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => [
|
|
'inventory_record_missing' => 1,
|
|
],
|
|
],
|
|
'diagnostics' => [
|
|
'support_only' => 'raw payload should stay hidden',
|
|
'provider_response' => 'provider response should stay hidden',
|
|
],
|
|
], OperationRunStatus::Completed->value, OperationRunOutcome::PartiallySucceeded->value);
|
|
|
|
Finding::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'scope_key' => 'baseline_profile:'.$profile->getKey(),
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
'status' => Finding::STATUS_NEW,
|
|
'source' => OperationRunType::BaselineCompare->value,
|
|
'baseline_operation_run_id' => (int) $run->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
baselineCompareLandingLivewire($environment)
|
|
->assertSee('Which baseline drift requires action?')
|
|
->assertSee('Drift requires review')
|
|
->assertSee('Evidence path')
|
|
->assertSee('Evidence gaps need review')
|
|
->assertSee('Open operation proof')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider response should stay hidden')
|
|
->assertDontSee('stack trace')
|
|
->assertDontSee('debug metadata');
|
|
});
|
|
|
|
it('keeps both surfaces environment-owned and rejects legacy compare entry points', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $environment->workspace_id => (int) $environment->getKey(),
|
|
],
|
|
])
|
|
->get(ManagedEnvironmentLinks::viewUrl($environment))
|
|
->assertOk()
|
|
->assertSeeText('Is this environment ready, blocked, stale, or requiring review?')
|
|
->assertDontSeeText('MANAGED_ENVIRONMENT');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
|
->get(ManagedEnvironmentLinks::baselineCompareUrl($environment))
|
|
->assertOk()
|
|
->assertSeeText('Which baseline drift requires action?')
|
|
->assertDontSeeText('MANAGED_ENVIRONMENT');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $environment->workspace_id => (int) $environment->getKey(),
|
|
],
|
|
])
|
|
->get('/admin/baseline-compare-landing?environment_id='.(int) $environment->getKey().'&tenant_scope=selected')
|
|
->assertNotFound();
|
|
|
|
expect(BaselineCompareLanding::getUrl([
|
|
'tenant' => (string) $environment->external_id,
|
|
'tenant_id' => (int) $environment->getKey(),
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'environment' => 'legacy-alias',
|
|
'tenant_scope' => 'selected',
|
|
'tableFilters' => ['managed_environment_id' => ['value' => (int) $environment->getKey()]],
|
|
], panel: 'admin', tenant: $environment))
|
|
->not->toContain('tenant_id=')
|
|
->not->toContain('managed_environment_id=')
|
|
->not->toContain('tenant_scope=')
|
|
->not->toContain('tableFilters');
|
|
});
|