TenantAtlas/tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.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

180 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Filament\Resources\TenantReviewResource;
use App\Filament\Widgets\Workspace\WorkspaceNeedsAttention;
use App\Models\AlertDelivery;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\Workspaces\WorkspaceOverviewBuilder;
use Livewire\Livewire;
it('preserves canonical findings, compare, alerts, and operations drill-through continuity from the workspace overview', function (): void {
$tenantDashboard = Tenant::factory()->create(['status' => 'active']);
[$user, $tenantDashboard] = createUserWithTenant($tenantDashboard, role: 'owner', workspaceRole: 'readonly');
[$dashboardProfile, $dashboardSnapshot] = seedActiveBaselineForTenant($tenantDashboard);
seedBaselineCompareRun($tenantDashboard, $dashboardProfile, $dashboardSnapshot, workspaceOverviewCompareCoverage());
Finding::factory()->riskAccepted()->create([
'workspace_id' => (int) $tenantDashboard->workspace_id,
'tenant_id' => (int) $tenantDashboard->getKey(),
]);
$tenantFindings = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantDashboard->workspace_id,
]);
createUserWithTenant($tenantFindings, $user, role: 'owner', workspaceRole: 'readonly');
[$findingsProfile, $findingsSnapshot] = seedActiveBaselineForTenant($tenantFindings);
seedBaselineCompareRun($tenantFindings, $findingsProfile, $findingsSnapshot, workspaceOverviewCompareCoverage());
Finding::factory()->for($tenantFindings)->create([
'workspace_id' => (int) $tenantFindings->workspace_id,
'status' => Finding::STATUS_TRIAGED,
'due_at' => now()->subDay(),
]);
$tenantCompare = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantDashboard->workspace_id,
'name' => 'Compare Tenant',
]);
createUserWithTenant($tenantCompare, $user, role: 'owner', workspaceRole: 'readonly');
[$compareProfile, $compareSnapshot] = seedActiveBaselineForTenant($tenantCompare);
seedBaselineCompareRun(
$tenantCompare,
$compareProfile,
$compareSnapshot,
workspaceOverviewCompareCoverage(),
completedAt: now()->subDays(10),
);
$tenantOperations = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantDashboard->workspace_id,
'name' => 'Operations Tenant',
]);
createUserWithTenant($tenantOperations, $user, role: 'owner', workspaceRole: 'readonly');
[$operationsProfile, $operationsSnapshot] = seedActiveBaselineForTenant($tenantOperations);
seedBaselineCompareRun($tenantOperations, $operationsProfile, $operationsSnapshot, workspaceOverviewCompareCoverage());
OperationRun::factory()->create([
'tenant_id' => (int) $tenantOperations->getKey(),
'workspace_id' => (int) $tenantOperations->workspace_id,
'type' => OperationRunType::PolicySync->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
]);
$tenantAlerts = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantDashboard->workspace_id,
'name' => 'Alerts Tenant',
]);
createUserWithTenant($tenantAlerts, $user, role: 'owner', workspaceRole: 'readonly');
[$alertsProfile, $alertsSnapshot] = seedActiveBaselineForTenant($tenantAlerts);
seedBaselineCompareRun($tenantAlerts, $alertsProfile, $alertsSnapshot, workspaceOverviewCompareCoverage());
AlertDelivery::factory()->create([
'tenant_id' => (int) $tenantAlerts->getKey(),
'workspace_id' => (int) $tenantAlerts->workspace_id,
'status' => AlertDelivery::STATUS_FAILED,
'created_at' => now(),
]);
$workspace = $tenantDashboard->workspace()->firstOrFail();
$overview = app(WorkspaceOverviewBuilder::class)->build($workspace, $user);
$items = collect($overview['attention_items'])->keyBy('key');
expect($items->get('tenant_lapsed_governance')['destination']['kind'])->toBe('tenant_dashboard')
->and($items->get('tenant_overdue_findings')['destination']['kind'])->toBe('tenant_findings')
->and($items->get('tenant_overdue_findings')['destination']['url'])->toContain('tab=overdue')
->and($items->get('tenant_compare_attention')['destination']['kind'])->toBe('baseline_compare_landing')
->and($items->get('tenant_operations_terminal_follow_up')['destination']['kind'])->toBe('operations_index')
->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('activeTab=terminal_follow_up')
->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('problemClass=terminal_follow_up')
->and($items->get('tenant_operations_terminal_follow_up')['destination']['url'])->toContain('tenant_id='.(string) $tenantOperations->getKey())
->and($items->get('tenant_alert_delivery_failures')['destination']['kind'])->toBe('alerts_overview')
->and($items->get('tenant_alert_delivery_failures')['destination']['url'])->toContain('nav%5Bback_url%5D=');
});
it('renders evidence and review actions through the shared attention-item contract', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'readonly');
$this->actingAs($user);
$evidenceUrl = EvidenceSnapshotResource::getUrl('index', panel: 'tenant', tenant: $tenant);
$reviewUrl = TenantReviewResource::tenantScopedUrl('index', [], $tenant);
$items = [
[
'key' => 'tenant_evidence_attention',
'tenant_id' => (int) $tenant->getKey(),
'tenant_label' => (string) $tenant->name,
'tenant_route_key' => (string) $tenant->external_id,
'family' => 'evidence',
'urgency' => 'high',
'title' => 'Evidence needs refresh',
'body' => 'Current evidence should be refreshed before relying on it.',
'supporting_message' => null,
'badge' => 'Evidence',
'badge_color' => 'warning',
'destination' => [
'kind' => 'tenant_evidence',
'url' => $evidenceUrl,
'tenant_route_key' => (string) $tenant->external_id,
'label' => 'Open evidence',
'disabled' => false,
'helper_text' => null,
'filters' => null,
],
'action_disabled' => false,
'helper_text' => null,
'url' => $evidenceUrl,
],
[
'key' => 'tenant_review_attention',
'tenant_id' => (int) $tenant->getKey(),
'tenant_label' => (string) $tenant->name,
'tenant_route_key' => (string) $tenant->external_id,
'family' => 'review',
'urgency' => 'medium',
'title' => 'Review needs publication work',
'body' => 'The current review is still internal-only.',
'supporting_message' => null,
'badge' => 'Review',
'badge_color' => 'warning',
'destination' => [
'kind' => 'tenant_reviews',
'url' => $reviewUrl,
'tenant_route_key' => (string) $tenant->external_id,
'label' => 'Open review',
'disabled' => false,
'helper_text' => null,
'filters' => null,
],
'action_disabled' => false,
'helper_text' => null,
'url' => $reviewUrl,
],
];
$component = Livewire::test(WorkspaceNeedsAttention::class, [
'items' => $items,
'emptyState' => [],
])
->assertSee('Evidence needs refresh')
->assertSee('Open evidence')
->assertSee('Review needs publication work')
->assertSee('Open review');
expect($component->html())
->toContain($evidenceUrl)
->toContain($reviewUrl);
});