TenantAtlas/apps/platform/tests/Feature/Filament/Spec369BaselineProfileDecisionViewTest.php
ahmido 54eb8ca065 feat(ui): implement baseline profile decision view (#440)
Added a decision-first section to the Baseline Profile detail page. Includes request caching for summary metrics and corresponding browser/feature tests.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #440
2026-06-10 12:11:55 +00:00

157 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\BaselineProfileResource;
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
uses(RefreshDatabase::class);
/**
* @return array{0: \App\Models\User, 1: \App\Models\ManagedEnvironment, 2: BaselineProfile, 3: BaselineSnapshot}
*/
function spec369ReadyProfile(): array
{
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
$profile->forceFill([
'name' => 'Spec369 Ready Baseline',
'capture_mode' => BaselineCaptureMode::Opportunistic->value,
])->save();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
return [$user, $tenant, $profile->fresh(), $snapshot->fresh()];
}
it('renders a ready baseline profile with a decision-first summary before technical scope', function (): void {
[$user, , $profile, $snapshot] = spec369ReadyProfile();
$this->actingAs($user)
->get(BaselineProfileResource::getUrl('view', ['record' => $profile], panel: 'admin'))
->assertOk()
->assertSee('Decision')
->assertSee('Ready to compare')
->assertSee('Current snapshot is consumable and at least one assigned environment is available for compare.')
->assertSee('Operators can compare assigned environments against the current baseline snapshot.')
->assertSee('Snapshot #'.$snapshot->getKey().' (Complete)')
->assertSee('Matches current snapshot')
->assertSee('Assigned to 1 environment with compare access.')
->assertSee('Dominant next action')
->assertSee('Compare now')
->assertSeeInOrder([
'Decision',
'Ready to compare',
'Baseline truth',
'Related context',
'Profile',
'Scope',
'Normalization lineage',
'Metadata',
]);
});
it('shows capture-oriented decision guidance when no consumable snapshot exists', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
'name' => 'Spec369 Capture Needed Baseline',
]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
$this->actingAs($user)
->get(BaselineProfileResource::getUrl('view', ['record' => $profile], panel: 'admin'))
->assertOk()
->assertSee('Decision')
->assertSee('Capture needed before compare')
->assertSee('Assignments can be prepared, but compare waits for a complete current snapshot.')
->assertSee('No complete snapshot')
->assertSee('No capture attempts yet')
->assertSee('Assigned to 1 environment with compare access.')
->assertSee('Capture baseline')
->assertSeeInOrder(['Decision', 'Capture needed before compare', 'Baseline truth', 'Scope']);
});
it('shows scope repair guidance for invalid governed-subject scope without exposing raw scope json', function (): void {
[$user, , $profile] = spec369ReadyProfile();
DB::table('baseline_profiles')
->where('id', (int) $profile->getKey())
->update([
'scope_jsonb' => json_encode([
'version' => 2,
'entries' => [],
], JSON_THROW_ON_ERROR),
'updated_at' => now(),
]);
$profile = $profile->fresh();
$this->actingAs($user)
->get(BaselineProfileResource::getUrl('view', ['record' => $profile], panel: 'admin'))
->assertOk()
->assertSee('Scope review required')
->assertSee('Stored governed-subject scope is invalid. Review the profile definition before compare.')
->assertSee('Capture and compare are blocked until governed-subject scope is reviewed.')
->assertSee('Edit profile definition')
->assertDontSee('subject_type_keys')
->assertDontSee('canonical_scope')
->assertSeeInOrder(['Decision', 'Scope review required', 'Scope', 'Stored scope is invalid']);
});
it('keeps readonly members on non-mutating decision copy and disabled high-impact actions', function (): void {
[$owner, $tenant, $profile] = spec369ReadyProfile();
[$readonly] = createUserWithTenant(tenant: $tenant, user: null, role: 'readonly', workspaceRole: 'readonly');
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::actingAs($readonly)
->test(ViewBaselineProfile::class, ['record' => $profile->getKey()])
->assertSee('Assignment or access required')
->assertSee('No assigned environment is available to this actor for compare.')
->assertSee('Open compare matrix')
->assertActionVisible('capture')
->assertActionDisabled('capture')
->assertActionVisible('compareNow')
->assertActionDisabled('compareNow');
$this->actingAs($owner);
});
it('keeps related snapshot and compare matrix context available below the decision', function (): void {
[$user, , $profile] = spec369ReadyProfile();
$this->actingAs($user);
$entryKeys = collect(BaselineProfileResource::detailRelatedContextEntries($profile))
->pluck('key')
->all();
expect($entryKeys)->toContain('baseline_snapshot', 'compare_matrix');
$this->get(BaselineProfileResource::getUrl('view', ['record' => $profile], panel: 'admin'))
->assertOk()
->assertSee('Related context')
->assertSee('View snapshot')
->assertSee('Open compare matrix')
->assertSeeInOrder(['Decision', 'Related context', 'View snapshot', 'Compare matrix', 'Open compare matrix']);
});