TenantAtlas/tests/Browser/Spec177InventoryCoverageTruthSmokeTest.php
ahmido 1142d283eb feat: Spec 178 — Operations Lifecycle Alignment & Cross-Surface Truth Consistency (#209)
## Spec 178 — Operations Lifecycle Alignment & Cross-Surface Truth Consistency

Härtet die Run-Lifecycle-Wahrheit und Cross-Surface-Konsistenz über alle zentralen Operator-Flächen hinweg.

### Kern-Änderungen

**Lifecycle Truth Alignment**
- Einheitliche stale/stuck-Semantik zwischen Tenant-, Workspace-, Admin- und System-Surfaces
- `OperationRunFreshnessState` wird konsistent über alle Widgets und Seiten propagiert
- Gemeinsame Problem-Klassen-Trennung: `terminal_follow_up` vs. `active_stale_attention`

**BulkOperationProgress Freshness**
- Overlay zeigt nur noch `healthyActive()` Runs statt alle aktiven Runs
- Likely-stale Runs halten das Polling nicht mehr künstlich aktiv
- Terminal Runs verschwinden zeitnah aus dem Progress-Overlay

**Decision Zone im Run Detail**
- Stale/reconciled Attention in der primären Decision-Hierarchie
- Klare Antworten: aktiv? stale? reconciled? nächster Schritt?
- Artifact-reiche Runs behalten Lifecycle-Truth vor Deep-Diagnostics

**Cross-Surface Link-Continuity**
- Dashboard → Operations Hub → Run Detail erzählen dieselbe Geschichte
- Notifications referenzieren korrekte Problem-Klasse
- Workspace/Tenant-Attention verlinken problemklassengerecht

**System-Plane Fixes**
- `/system/ops/failures` 500-Error behoben (panel-sichere Artifact-URLs)
- System-Stuck/Failures zeigen reconciled stale lineage

### Weitere Fixes
- Inventory auth guard bereinigt (Gate statt ad-hoc Facades)
- Browser-Smoke-Tests stabilisiert (DOM-Assertions statt fragile Klicks)
- Test-Assertion-Drift für Verification/Lifecycle-Texte korrigiert

### Test-Ergebnis
Full Suite: **3269 passed**, 8 skipped, 0 failed

### Spec-Artefakte
- `specs/178-ops-truth-alignment/spec.md`
- `specs/178-ops-truth-alignment/plan.md`
- `specs/178-ops-truth-alignment/tasks.md`
- `specs/178-ops-truth-alignment/research.md`
- `specs/178-ops-truth-alignment/data-model.md`
- `specs/178-ops-truth-alignment/quickstart.md`
- `specs/178-ops-truth-alignment/contracts/operations-truth-alignment.openapi.yaml`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #209
2026-04-05 22:42:24 +00:00

274 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\InventoryCoverage;
use App\Filament\Resources\InventoryItemResource;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(15_000);
function inventoryItemListLivewireScript(string $body): string
{
return <<<JS
const component = window.Livewire?.all().find((entry) => {
if (entry.name === 'App\\Filament\\Resources\\InventoryItemResource\\Pages\\ListInventoryItems') {
return true;
}
const state = entry.canonical ?? {};
return Object.prototype.hasOwnProperty.call(state, 'tableFilters')
&& Object.prototype.hasOwnProperty.call(state, 'tableDeferredFilters')
&& Object.prototype.hasOwnProperty.call(state, 'tableRecordsPerPage')
&& Object.prototype.hasOwnProperty.call(state, 'tableSearch');
});
if (! component) {
throw new Error('ListInventoryItems Livewire component not found.');
}
{$body}
JS;
}
function seedSpec177InventoryCoverageTruthFixtures(Tenant $tenant): OperationRun
{
foreach (range(1, 130) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Browser Inventory %02d', $index),
'policy_type' => 'deviceConfiguration',
'external_id' => sprintf('browser-inventory-%02d', $index),
'platform' => 'windows',
'last_seen_at' => now()->subMinutes($index),
]);
}
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => 'Browser Conditional Access',
'policy_type' => 'conditionalAccessPolicy',
'external_id' => 'browser-conditional-access',
'platform' => 'windows',
'last_seen_at' => now()->subMinutes(31),
]);
return createInventorySyncOperationRunWithCoverage(
$tenant,
[
'conditionalAccessPolicy' => 'succeeded',
'deviceConfiguration' => 'failed',
'roleScopeTag' => 'skipped',
],
['roleScopeTag'],
[
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
'completed_at' => now()->subMinute(),
],
);
}
function seedSpec177InventoryItemFilterPaginationFixtures(Tenant $tenant): void
{
foreach (range(1, 45) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Windows Fresh Device %02d', $index),
'policy_type' => 'deviceConfiguration',
'external_id' => sprintf('windows-fresh-device-%02d', $index),
'platform' => 'windows',
'last_seen_at' => now()->subMinutes($index),
]);
}
foreach (range(1, 3) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Mac Fresh Device %02d', $index),
'policy_type' => 'deviceConfiguration',
'external_id' => sprintf('mac-fresh-device-%02d', $index),
'platform' => 'macOS',
'last_seen_at' => now()->subMinutes(45 + $index),
]);
}
foreach (range(1, 3) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Conditional Access Fresh %02d', $index),
'policy_type' => 'conditionalAccessPolicy',
'external_id' => sprintf('conditional-access-fresh-%02d', $index),
'platform' => 'windows',
'last_seen_at' => now()->subMinutes(48 + $index),
]);
}
foreach (range(46, 55) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Windows Fresh Device %02d', $index),
'policy_type' => 'deviceConfiguration',
'external_id' => sprintf('windows-fresh-device-%02d', $index),
'platform' => 'windows',
'last_seen_at' => now()->subMinutes(6 + $index),
]);
}
foreach (range(1, 3) as $index) {
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => sprintf('Windows Stale Device %02d', $index),
'policy_type' => 'deviceConfiguration',
'external_id' => sprintf('windows-stale-device-%02d', $index),
'platform' => 'windows',
'last_seen_at' => now()->subDays(3)->subMinutes($index),
]);
}
}
it('smokes inventory coverage truth surfaces with filters, pagination, and run drill-through', function (): void {
$tenant = Tenant::factory()->create([
'name' => 'Spec177 Browser Tenant',
'external_id' => 'spec177-browser-tenant',
]);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$run = seedSpec177InventoryCoverageTruthFixtures($tenant);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
$coverageUrl = InventoryCoverage::getUrl(tenant: $tenant);
$basisRunUrl = OperationRunLinks::view($run, $tenant);
$inventoryItemsUrl = InventoryItemResource::getUrl('index', tenant: $tenant);
$searchPage = visit(InventoryItemResource::getUrl('index', tenant: $tenant));
$searchPage
->waitForText('Inventory Items')
->assertNoJavaScriptErrors()
->assertSee('Covered types')
->assertSee('Need follow-up')
->assertSee('Coverage basis')
->assertSee('Open basis run')
->assertSee('Run Inventory Sync')
->assertSee('Browser Inventory 01')
->assertDontSee('Browser Inventory 130')
->fill('main input[placeholder="Search"]', 'Browser Inventory 01')
->waitForText('Browser Inventory 01')
->assertNoJavaScriptErrors()
->assertSee('Browser Inventory 01')
->assertDontSee('Browser Inventory 02');
$page = visit($coverageUrl);
$page
->waitForText('Tenant coverage truth')
->assertNoJavaScriptErrors()
->assertSee('Latest coverage-bearing sync completed')
->assertSee('Open basis run')
->assertSee('Open inventory items')
->fill('input[placeholder="Search by type or label"]', 'Conditional Access')
->waitForText('Conditional Access')
->assertNoJavaScriptErrors()
->assertSee('Conditional Access')
->assertScript("document.querySelector('input[placeholder=\"Search by type or label\"]')?.value === 'Conditional Access'", true)
->assertScript("Array.from(document.querySelectorAll('a[href=\"{$basisRunUrl}\"]')).some((element) => element.textContent?.includes('Open basis run'))", true);
visit($basisRunUrl)
->waitForText('Operation #'.(int) $run->getKey())
->assertNoJavaScriptErrors()
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()])
->assertSee('Inventory sync coverage')
->assertSee('Need follow-up');
visit($coverageUrl)
->waitForText('Tenant coverage truth')
->assertNoJavaScriptErrors()
->assertScript("Array.from(document.querySelectorAll('a[href=\"{$inventoryItemsUrl}\"]')).some((element) => element.textContent?.includes('Open inventory items'))", true);
visit($inventoryItemsUrl)
->waitForText('Inventory Items')
->assertNoJavaScriptErrors()
->assertSee('Browser Inventory 01');
});
it('smokes inventory item pagination with stable filter combinations', function (): void {
$tenant = Tenant::factory()->create([
'name' => 'Spec177 Browser Filter Tenant',
'external_id' => 'spec177-browser-filter-tenant',
]);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
seedSpec177InventoryItemFilterPaginationFixtures($tenant);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
$page = visit(InventoryItemResource::getUrl('index', tenant: $tenant));
$page
->waitForText('Inventory Items')
->assertNoJavaScriptErrors()
->assertSee('Windows Fresh Device 01')
->assertDontSee('Windows Fresh Device 30')
->assertDontSee('Mac Fresh Device 01')
->wait(1);
$page->script(inventoryItemListLivewireScript(<<<'JS'
component.$wire.set('tableRecordsPerPage', 50);
JS));
$page
->wait(1)
->waitForText('Mac Fresh Device 01')
->assertNoJavaScriptErrors()
->assertSee('Mac Fresh Device 01')
->assertSee('Conditional Access Fresh 01')
->assertDontSee('Windows Fresh Device 46');
$page->script(inventoryItemListLivewireScript(<<<'JS'
component.$wire.set('tableDeferredFilters.policy_type.value', 'deviceConfiguration');
component.$wire.set('tableDeferredFilters.platform.value', 'windows');
component.$wire.set('tableDeferredFilters.stale.value', '0');
component.$wire.call('applyTableFilters');
JS));
$page
->wait(1)
->waitForText('Active filters')
->waitForText('Windows Fresh Device 46')
->assertNoJavaScriptErrors()
->assertSee('Policy type: Device Configuration')
->assertSee('Platform: Windows')
->assertSee('Freshness: Fresh')
->assertSee('Windows Fresh Device 46')
->assertDontSee('Mac Fresh Device 01')
->assertDontSee('Conditional Access Fresh 01')
->assertDontSee('Windows Stale Device 01');
$page->script(inventoryItemListLivewireScript(<<<'JS'
component.$wire.call('gotoPage', 2);
JS));
$page
->wait(1)
->waitForText('Windows Fresh Device 51')
->assertNoJavaScriptErrors()
->assertSee('Windows Fresh Device 51')
->assertDontSee('Windows Fresh Device 01')
->assertDontSee('Mac Fresh Device 01');
});