TenantAtlas/apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php
ahmido 74210bac2e feat: add baseline compare operator modes (#224)
## 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
2026-04-11 15:48:22 +00:00

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');
});