TenantAtlas/apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php
ahmido eca19819d1 feat: add workspace baseline compare matrix (#221)
## Summary
- add a workspace-scoped baseline compare matrix page under baseline profiles
- derive matrix tenant summaries, subject rows, cell states, freshness, and trust from existing snapshots, compare runs, and findings
- add confirmation-gated `Compare assigned tenants` actions on the baseline detail and matrix surfaces without introducing a workspace umbrella run
- preserve matrix navigation context into tenant compare and finding drilldowns and add centralized matrix badge semantics
- include spec, plan, data model, contracts, quickstart, tasks, and focused feature/browser coverage for Spec 190

## Verification
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- completed an integrated-browser smoke flow locally for matrix render, differ filter, finding drilldown round-trip, and `Compare assigned tenants` confirmation/action

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #221
2026-04-11 10:20:25 +00:00

255 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
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 tenant and subject summaries 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['rows'])->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(count($wifiRow['cells']))->toBe(2);
});
it('derives matrix cell precedence from compare freshness, evidence gaps, findings, and uncovered policy types', 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['rows'])->first(
static fn (array $row): bool => ($row['subject']['subjectKey'] ?? null) === 'wifi-corp-profile',
);
$statesByTenant = collect($wifiRow['cells'] ?? [])
->mapWithKeys(static fn (array $cell): array => [(int) $cell['tenantId'] => (string) $cell['state']])
->all();
expect($statesByTenant[(int) $matchTenant->getKey()] ?? null)->toBe('match')
->and($statesByTenant[(int) $differTenant->getKey()] ?? null)->toBe('differ')
->and($statesByTenant[(int) $missingTenant->getKey()] ?? null)->toBe('missing')
->and($statesByTenant[(int) $ambiguousTenant->getKey()] ?? null)->toBe('ambiguous')
->and($statesByTenant[(int) $notComparedTenant->getKey()] ?? null)->toBe('not_compared')
->and($statesByTenant[(int) $staleTenant->getKey()] ?? null)->toBe('stale_result');
});
it('applies policy-type, state, severity, and subject-focus filters honestly', 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['rows']))->toBe(1)
->and($deviceOnly['rows'][0]['subject']['policyType'])->toBe('deviceConfiguration')
->and(count($driftOnly['rows']))->toBe(1)
->and($driftOnly['rows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile')
->and(count($subjectFocus['rows']))->toBe(1)
->and($subjectFocus['rows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile');
});