## Summary - add adaptive baseline compare presentation modes with `auto`, `dense`, and `compact` route handling on the existing matrix page - compress support surfaces with staged filters, grouped legends, last-updated and passive refresh cues, compact single-tenant results, and dense multi-tenant scan rendering - extend the matrix builder plus Pest and browser smoke coverage for visible-set-only compact and dense workflows ## Filament / Laravel notes - Livewire v4 compliance preserved; no legacy Livewire v3 patterns introduced - provider registration is unchanged; no `bootstrap/providers.php` changes were needed for this feature - no globally searchable resources were changed by this branch - no destructive actions were added; the existing compare action remains simulation-only and non-destructive - asset strategy is unchanged; no new Filament assets were introduced ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php` - `80` tests passed with `673` assertions - integrated browser smoke run on `http://localhost/admin/baseline-profiles/20/compare-matrix` ## Scope - Spec 191 implementation - spec contract updates in `spec.md`, `tasks.md`, and the logical OpenAPI contract Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #224
318 lines
13 KiB
PHP
318 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\Baselines\BaselineCompareMatrixBuilder;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures;
|
|
|
|
uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class);
|
|
|
|
it('builds visible-set-only dense rows plus support metadata from assigned baseline truth', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$visibleRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$visibleRunTwo = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$hiddenRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['hiddenTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$visibleRunTwo,
|
|
'wifi-corp-profile',
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['hiddenTenant'],
|
|
$fixture['profile'],
|
|
$hiddenRun,
|
|
'wifi-corp-profile',
|
|
['severity' => 'critical'],
|
|
);
|
|
|
|
$matrix = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user']);
|
|
|
|
$wifiRow = collect($matrix['denseRows'])->first(
|
|
static fn (array $row): bool => ($row['subject']['subjectKey'] ?? null) === 'wifi-corp-profile',
|
|
);
|
|
|
|
expect($matrix['reference']['assignedTenantCount'])->toBe(3)
|
|
->and($matrix['reference']['visibleTenantCount'])->toBe(2)
|
|
->and(collect($matrix['tenantSummaries'])->pluck('tenantName')->all())->toEqualCanonicalizing([
|
|
(string) $fixture['visibleTenant']->name,
|
|
(string) $fixture['visibleTenantTwo']->name,
|
|
])
|
|
->and($wifiRow)->not->toBeNull()
|
|
->and($wifiRow['subject']['deviationBreadth'])->toBe(1)
|
|
->and($wifiRow['subject']['attentionLevel'])->toBe('needs_attention')
|
|
->and(count($wifiRow['cells']))->toBe(2)
|
|
->and($matrix['denseRows'])->toHaveCount(2)
|
|
->and($matrix['compactResults'])->toBeEmpty()
|
|
->and($matrix['supportSurfaceState']['legendMode'])->toBe('grouped')
|
|
->and($matrix['supportSurfaceState']['showAutoRefreshHint'])->toBeFalse()
|
|
->and($matrix['lastUpdatedAt'])->not->toBeNull();
|
|
});
|
|
|
|
it('derives matrix cell precedence, freshness, attention, and reason summaries from compare truth', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$matchTenant = $fixture['visibleTenant'];
|
|
$differTenant = $fixture['visibleTenantTwo'];
|
|
|
|
$missingTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Missing',
|
|
]);
|
|
$ambiguousTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Ambiguous',
|
|
]);
|
|
$notComparedTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Uncovered',
|
|
]);
|
|
$staleTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Stale',
|
|
]);
|
|
|
|
$fixture['user']->tenants()->syncWithoutDetaching([
|
|
(int) $missingTenant->getKey() => ['role' => 'owner'],
|
|
(int) $ambiguousTenant->getKey() => ['role' => 'owner'],
|
|
(int) $notComparedTenant->getKey() => ['role' => 'owner'],
|
|
(int) $staleTenant->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $missingTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $ambiguousTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $notComparedTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $staleTenant);
|
|
|
|
$this->makeBaselineCompareMatrixRun($matchTenant, $fixture['profile'], $fixture['snapshot']);
|
|
|
|
$differRun = $this->makeBaselineCompareMatrixRun($differTenant, $fixture['profile'], $fixture['snapshot']);
|
|
$this->makeBaselineCompareMatrixFinding($differTenant, $fixture['profile'], $differRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'different_version',
|
|
],
|
|
]);
|
|
|
|
$missingRun = $this->makeBaselineCompareMatrixRun($missingTenant, $fixture['profile'], $fixture['snapshot']);
|
|
$this->makeBaselineCompareMatrixFinding($missingTenant, $fixture['profile'], $missingRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'missing_policy',
|
|
],
|
|
]);
|
|
|
|
$ambiguousRun = $this->makeBaselineCompareMatrixRun(
|
|
$ambiguousTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[
|
|
'baseline_compare' => [
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => ['ambiguous_match' => 1],
|
|
'subjects' => [
|
|
$this->baselineCompareMatrixGap('deviceConfiguration', 'wifi-corp-profile', [
|
|
'reason_code' => 'ambiguous_match',
|
|
'resolution_outcome' => 'ambiguous_match',
|
|
]),
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
$this->makeBaselineCompareMatrixFinding($ambiguousTenant, $fixture['profile'], $ambiguousRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'missing_policy',
|
|
],
|
|
]);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$notComparedTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[
|
|
'baseline_compare' => [
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => [],
|
|
'uncovered_types' => ['deviceConfiguration'],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$staleTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[],
|
|
[
|
|
'completed_at' => $fixture['snapshot']->captured_at->copy()->subDay(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $fixture['profile']->getKey(),
|
|
'baseline_snapshot_id' => (int) $fixture['snapshot']->getKey() - 1,
|
|
'baseline_compare' => [
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 0,
|
|
'by_reason' => [],
|
|
'subjects' => [],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$matrix = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user']);
|
|
|
|
$wifiRow = collect($matrix['denseRows'])->first(
|
|
static fn (array $row): bool => ($row['subject']['subjectKey'] ?? null) === 'wifi-corp-profile',
|
|
);
|
|
|
|
$cellsByTenant = collect($wifiRow['cells'] ?? [])
|
|
->mapWithKeys(static fn (array $cell): array => [(int) $cell['tenantId'] => $cell])
|
|
->all();
|
|
|
|
expect($cellsByTenant[(int) $matchTenant->getKey()]['state'] ?? null)->toBe('match')
|
|
->and($cellsByTenant[(int) $matchTenant->getKey()]['attentionLevel'] ?? null)->toBe('aligned')
|
|
->and($cellsByTenant[(int) $differTenant->getKey()]['state'] ?? null)->toBe('differ')
|
|
->and($cellsByTenant[(int) $differTenant->getKey()]['attentionLevel'] ?? null)->toBe('needs_attention')
|
|
->and($cellsByTenant[(int) $differTenant->getKey()]['reasonSummary'] ?? null)->toBe('A baseline compare finding exists for this subject.')
|
|
->and($cellsByTenant[(int) $missingTenant->getKey()]['state'] ?? null)->toBe('missing')
|
|
->and($cellsByTenant[(int) $missingTenant->getKey()]['attentionLevel'] ?? null)->toBe('needs_attention')
|
|
->and($cellsByTenant[(int) $ambiguousTenant->getKey()]['state'] ?? null)->toBe('ambiguous')
|
|
->and($cellsByTenant[(int) $ambiguousTenant->getKey()]['reasonSummary'] ?? null)->toBe('Ambiguous Match')
|
|
->and($cellsByTenant[(int) $notComparedTenant->getKey()]['state'] ?? null)->toBe('not_compared')
|
|
->and($cellsByTenant[(int) $notComparedTenant->getKey()]['attentionLevel'] ?? null)->toBe('refresh_recommended')
|
|
->and($cellsByTenant[(int) $notComparedTenant->getKey()]['reasonSummary'] ?? null)->toContain('Policy type coverage was not proven')
|
|
->and($cellsByTenant[(int) $staleTenant->getKey()]['state'] ?? null)->toBe('stale_result')
|
|
->and($cellsByTenant[(int) $staleTenant->getKey()]['freshnessState'] ?? null)->toBe('stale')
|
|
->and($cellsByTenant[(int) $staleTenant->getKey()]['trustLevel'] ?? null)->toBe('limited_confidence')
|
|
->and($cellsByTenant[(int) $staleTenant->getKey()]['attentionLevel'] ?? null)->toBe('refresh_recommended');
|
|
});
|
|
|
|
it('applies policy, state, severity, and subject-focus filters honestly without changing compare truth', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$visibleRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[],
|
|
[
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$visibleRun,
|
|
'wifi-corp-profile',
|
|
['severity' => 'critical'],
|
|
);
|
|
|
|
$deviceOnly = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'policyTypes' => ['deviceConfiguration'],
|
|
]);
|
|
|
|
$driftOnly = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'states' => ['differ'],
|
|
'severities' => ['critical'],
|
|
]);
|
|
|
|
$subjectFocus = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'focusedSubjectKey' => 'wifi-corp-profile',
|
|
]);
|
|
|
|
expect(count($deviceOnly['denseRows']))->toBe(1)
|
|
->and($deviceOnly['denseRows'][0]['subject']['policyType'])->toBe('deviceConfiguration')
|
|
->and(count($driftOnly['denseRows']))->toBe(1)
|
|
->and($driftOnly['denseRows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile')
|
|
->and(count($subjectFocus['denseRows']))->toBe(1)
|
|
->and($subjectFocus['denseRows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile');
|
|
});
|
|
|
|
it('emits compact single-tenant results from the visible set only when one tenant remains in scope', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$run = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$run,
|
|
'wifi-corp-profile',
|
|
['severity' => 'critical'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$viewer = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'user_id' => (int) $viewer->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
$viewer->tenants()->syncWithoutDetaching([
|
|
(int) $fixture['visibleTenant']->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
$matrix = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $viewer);
|
|
|
|
expect($matrix['reference']['assignedTenantCount'])->toBe(3)
|
|
->and($matrix['reference']['visibleTenantCount'])->toBe(1)
|
|
->and($matrix['compactResults'])->toHaveCount(2)
|
|
->and(collect($matrix['compactResults'])->pluck('tenantId')->unique()->all())->toBe([(int) $fixture['visibleTenant']->getKey()])
|
|
->and(collect($matrix['compactResults'])->firstWhere('subjectKey', 'wifi-corp-profile')['state'] ?? null)->toBe('differ')
|
|
->and(collect($matrix['compactResults'])->firstWhere('subjectKey', 'wifi-corp-profile')['attentionLevel'] ?? null)->toBe('needs_attention');
|
|
});
|