TenantAtlas/tests/Feature/Filament/DerivedStateMutationFreshnessTest.php
ahmido d98dc30520 feat: add request-scoped derived state memoization (#198)
## Summary
- add a request-scoped derived-state store with deterministic keying and freshness controls
- adopt the shared contract in ArtifactTruthPresenter, OperationUxPresenter, and RelatedNavigationResolver plus the covered Filament consumers
- add spec, plan, contracts, guardrails, and focused memoization and freshness test coverage for spec 167

## Verification
- vendor/bin/sail artisan test --compact tests/Feature/078/RelatedLinksOnDetailTest.php
- vendor/bin/sail artisan test --compact tests/Feature/078/ tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Verification/VerificationAuthorizationTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php tests/Feature/Verification/VerificationReportRedactionTest.php tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php tests/Feature/OpsUx/FailureSanitizationTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php
- vendor/bin/sail bin pint --dirty --format agent

## Notes
- Livewire v4.0+ compliance preserved
- provider registration remains in bootstrap/providers.php
- no Filament assets or panel registration changes
- no global-search behavior changes
- no destructive action behavior changes in this PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #198
2026-03-28 14:58:30 +00:00

199 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots;
use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ReviewPack;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Navigation\CrossResourceNavigationMatrix;
use App\Support\Navigation\RelatedNavigationResolver;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\ReviewPackStatus;
use App\Support\Ui\DerivedState\DerivedStateFamily;
use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('refreshes evidence artifact truth after expiring a snapshot in the same request', function (): void {
$tenant = \App\Models\Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = EvidenceSnapshot::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'status' => EvidenceSnapshotStatus::Active->value,
'completeness_state' => EvidenceCompletenessState::Complete->value,
'summary' => [
'dimension_count' => 5,
'missing_dimensions' => 0,
'stale_dimensions' => 0,
],
'generated_at' => now(),
]);
Filament::setTenant($tenant, true);
Livewire::actingAs($user)
->test(ListEvidenceSnapshots::class)
->callTableAction('expire', $snapshot);
$snapshot->refresh();
$truth = app(ArtifactTruthPresenter::class)->forEvidenceSnapshot($snapshot);
expect((string) $snapshot->status)->toBe(EvidenceSnapshotStatus::Expired->value)
->and($truth->freshnessState)->toBe('stale')
->and(app(RequestScopedDerivedStateStore::class)->countStored(
DerivedStateFamily::ArtifactTruth,
EvidenceSnapshot::class,
(string) $snapshot->getKey(),
'evidence_snapshot',
))->toBe(1);
});
it('refreshes review-pack artifact truth after expiring a pack in the same request', function (): void {
$tenant = \App\Models\Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$pack = ReviewPack::factory()->ready()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'initiated_by_user_id' => (int) $user->getKey(),
'file_disk' => 'exports',
'file_path' => 'review-packs/freshness.zip',
]);
\Illuminate\Support\Facades\Storage::fake('exports');
\Illuminate\Support\Facades\Storage::disk('exports')->put('review-packs/freshness.zip', 'PK-fake');
Filament::setTenant($tenant, true);
Livewire::actingAs($user)
->test(ListReviewPacks::class)
->callTableAction('expire', $pack);
$pack->refresh();
$truth = app(ArtifactTruthPresenter::class)->forReviewPack($pack);
expect((string) $pack->status)->toBe(ReviewPackStatus::Expired->value)
->and($truth->freshnessState)->toBe('stale')
->and(app(RequestScopedDerivedStateStore::class)->countStored(
DerivedStateFamily::ArtifactTruth,
ReviewPack::class,
(string) $pack->getKey(),
'review_pack',
))->toBe(1);
});
it('returns fresh operation guidance after run state changes within the same request', function (): void {
$tenant = \App\Models\Tenant::factory()->create();
createUserWithTenant(tenant: $tenant, role: 'owner');
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
]);
$queuedGuidance = OperationUxPresenter::surfaceGuidance($run);
$run->update([
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'reason_code' => 'missing_capability',
'execution_legitimacy' => [
'reason_code' => 'missing_capability',
],
],
'failure_summary' => [[
'reason_code' => 'missing_capability',
'message' => 'Missing capability prevented execution.',
]],
]);
$staleGuidance = OperationUxPresenter::surfaceGuidance($run->fresh());
$freshGuidance = OperationUxPresenter::surfaceGuidanceFresh($run->fresh());
expect($staleGuidance)->toBe($queuedGuidance)
->and($freshGuidance)->not->toBe($staleGuidance)
->and($freshGuidance)->toBe('Review the blocked prerequisite before retrying.')
->and(app(RequestScopedDerivedStateStore::class)->countStored(
DerivedStateFamily::OperationUxGuidance,
OperationRun::class,
(string) $run->getKey(),
'surface_guidance',
))->toBe(1);
});
it('returns fresh related navigation after a finding changes its related policy version', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$policy = Policy::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
]);
$versionA = PolicyVersion::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'policy_id' => (int) $policy->getKey(),
'version_number' => 1,
]);
$versionB = PolicyVersion::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'policy_id' => (int) $policy->getKey(),
'version_number' => 2,
]);
$finding = Finding::factory()->for($tenant)->create([
'evidence_jsonb' => [
'current' => [
'policy_version_id' => (int) $versionA->getKey(),
],
],
]);
$resolver = app(RelatedNavigationResolver::class);
$firstEntries = collect($resolver->detailEntries(CrossResourceNavigationMatrix::SOURCE_FINDING, $finding));
$first = $firstEntries->firstWhere('key', 'current_policy_version');
$finding->update([
'evidence_jsonb' => [
'current' => [
'policy_version_id' => (int) $versionB->getKey(),
],
],
]);
$staleEntries = collect($resolver->detailEntries(CrossResourceNavigationMatrix::SOURCE_FINDING, $finding->fresh()));
$freshEntries = collect($resolver->detailEntriesFresh(CrossResourceNavigationMatrix::SOURCE_FINDING, $finding->fresh()));
$stale = $staleEntries->firstWhere('key', 'current_policy_version');
$fresh = $freshEntries->firstWhere('key', 'current_policy_version');
expect($first['targetUrl'] ?? null)->toBe($stale['targetUrl'] ?? null)
->and($fresh['targetUrl'] ?? null)->not->toBe($stale['targetUrl'] ?? null)
->and($fresh['targetUrl'] ?? null)->toContain((string) $versionB->getKey())
->and(app(RequestScopedDerivedStateStore::class)->countStored(
DerivedStateFamily::RelatedNavigationDetail,
Finding::class,
(string) $finding->getKey(),
CrossResourceNavigationMatrix::SOURCE_FINDING,
))->toBe(1);
});