## Summary - add the structured subject-resolution foundation for baseline compare and baseline capture, including capability guards, subject descriptors, resolution outcomes, and operator action categories - persist structured evidence-gap subject records and update compare/capture surfaces, landing projections, and cleanup tooling to use the new contract - add Spec 163 artifacts and focused Pest coverage for classification, determinism, cleanup, and DB-only rendering ## Validation - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Unit/Support/Baselines tests/Feature/Baselines tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php` ## Notes - verified locally that a fresh post-restart baseline compare run now writes structured `baseline_compare.evidence_gaps.subjects` records instead of the legacy broad payload shape - excluded the separate `docs/product/spec-candidates.md` worktree change from this branch commit and PR Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #193
150 lines
6.1 KiB
PHP
150 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Livewire\BaselineCompareEvidenceGapTable;
|
|
use App\Support\Badges\TagBadgeCatalog;
|
|
use App\Support\Badges\TagBadgeDomain;
|
|
use App\Support\Baselines\BaselineCompareEvidenceGapDetails;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Features\SupportTesting\Testable;
|
|
use Livewire\Livewire;
|
|
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function baselineCompareEvidenceGapTable(Testable $component): Table
|
|
{
|
|
return $component->instance()->getTable();
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
function baselineCompareEvidenceGapBuckets(): array
|
|
{
|
|
return BaselineCompareEvidenceGapDetails::fromContext(BaselineSubjectResolutionFixtures::compareContext([
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'WiFi-Corp-Profile',
|
|
'subject_class' => 'policy_backed',
|
|
'resolution_path' => 'policy',
|
|
'resolution_outcome' => 'ambiguous_match',
|
|
'reason_code' => 'ambiguous_match',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'VPN-Always-On',
|
|
'subject_class' => 'policy_backed',
|
|
'resolution_path' => 'policy',
|
|
'resolution_outcome' => 'ambiguous_match',
|
|
'reason_code' => 'ambiguous_match',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceCompliancePolicy',
|
|
'subject_key' => 'Windows-Encryption-Required',
|
|
'subject_class' => 'policy_backed',
|
|
'resolution_path' => 'policy',
|
|
'resolution_outcome' => 'ambiguous_match',
|
|
'reason_code' => 'ambiguous_match',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'Deleted-Policy-ABC',
|
|
'resolution_outcome' => 'policy_record_missing',
|
|
'reason_code' => 'policy_record_missing',
|
|
'operator_action_category' => 'run_policy_sync_or_backup',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceCompliancePolicy',
|
|
'subject_key' => 'Retired-Compliance-Policy',
|
|
'resolution_outcome' => 'policy_record_missing',
|
|
'reason_code' => 'policy_record_missing',
|
|
'operator_action_category' => 'run_policy_sync_or_backup',
|
|
]),
|
|
]))['buckets'];
|
|
}
|
|
|
|
it('uses a Filament table for evidence-gap rows with searchable visible columns', function (): void {
|
|
$component = Livewire::test(BaselineCompareEvidenceGapTable::class, [
|
|
'buckets' => baselineCompareEvidenceGapBuckets(),
|
|
'context' => 'canonical-run',
|
|
]);
|
|
|
|
$table = baselineCompareEvidenceGapTable($component);
|
|
|
|
expect($table->isSearchable())->toBeTrue();
|
|
expect($table->getDefaultSortColumn())->toBe('reason_label');
|
|
expect($table->getColumn('reason_label')?->isSearchable())->toBeTrue();
|
|
expect($table->getColumn('policy_type')?->isSearchable())->toBeTrue();
|
|
expect($table->getColumn('subject_class_label')?->isSearchable())->toBeTrue();
|
|
expect($table->getColumn('resolution_outcome_label')?->isSearchable())->toBeTrue();
|
|
expect($table->getColumn('operator_action_category_label')?->isSearchable())->toBeTrue();
|
|
expect($table->getColumn('subject_key')?->isSearchable())->toBeTrue();
|
|
|
|
$component
|
|
->assertSee('WiFi-Corp-Profile')
|
|
->assertSee('Deleted-Policy-ABC')
|
|
->assertSee('Reason')
|
|
->assertSee('Policy type')
|
|
->assertSee('Subject class')
|
|
->assertSee('Outcome')
|
|
->assertSee('Next action')
|
|
->assertSee('Subject key')
|
|
->assertSee('Policy-backed')
|
|
->assertSee('Policy record missing')
|
|
->assertSee('Run policy sync or backup')
|
|
->assertSee(TagBadgeCatalog::spec(TagBadgeDomain::PolicyType, 'deviceConfiguration')->label)
|
|
->assertSee(TagBadgeCatalog::spec(TagBadgeDomain::PolicyType, 'deviceCompliancePolicy')->label);
|
|
});
|
|
|
|
it('filters evidence-gap rows by table search and select filters', function (): void {
|
|
Livewire::test(BaselineCompareEvidenceGapTable::class, [
|
|
'buckets' => baselineCompareEvidenceGapBuckets(),
|
|
'context' => 'tenant-landing',
|
|
])
|
|
->searchTable('Deleted-Policy-ABC')
|
|
->assertSee('Deleted-Policy-ABC')
|
|
->assertDontSee('WiFi-Corp-Profile');
|
|
|
|
Livewire::test(BaselineCompareEvidenceGapTable::class, [
|
|
'buckets' => baselineCompareEvidenceGapBuckets(),
|
|
'context' => 'tenant-landing-filters',
|
|
])
|
|
->filterTable('reason_code', 'policy_record_missing')
|
|
->assertSee('Retired-Compliance-Policy')
|
|
->assertDontSee('VPN-Always-On')
|
|
->filterTable('policy_type', 'deviceCompliancePolicy')
|
|
->assertSee('Retired-Compliance-Policy')
|
|
->assertDontSee('Deleted-Policy-ABC')
|
|
->filterTable('operator_action_category', 'run_policy_sync_or_backup')
|
|
->assertSee('Run policy sync or backup')
|
|
->filterTable('subject_class', 'policy_backed')
|
|
->assertSee('Policy-backed');
|
|
});
|
|
|
|
it('shows an explicit empty state when only missing-detail buckets exist', function (): void {
|
|
$buckets = BaselineCompareEvidenceGapDetails::fromContext([
|
|
'baseline_compare' => [
|
|
'evidence_gaps' => [
|
|
'count' => 2,
|
|
'by_reason' => [
|
|
'policy_record_missing' => 2,
|
|
],
|
|
'subjects' => [],
|
|
],
|
|
],
|
|
])['buckets'];
|
|
|
|
Livewire::test(BaselineCompareEvidenceGapTable::class, [
|
|
'buckets' => $buckets,
|
|
'context' => 'legacy-run',
|
|
])
|
|
->assertSee('No recorded gap rows match this view')
|
|
->assertSee('Adjust the current search or filters to review other affected subjects.');
|
|
});
|