TenantAtlas/tests/Unit/Support/Ui/DerivedState/RequestScopedDerivedStateStoreTest.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

169 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Support\Ui\DerivedState\DerivedStateFamily;
use App\Support\Ui\DerivedState\DerivedStateKey;
use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore;
use Illuminate\Database\Eloquent\Model;
it('builds stable key fingerprints for equivalent context payloads', function (): void {
$record = new class extends Model
{
protected $guarded = [];
public $timestamps = false;
};
$record->forceFill([
'id' => 42,
'workspace_id' => 12,
'tenant_id' => 8,
]);
$left = DerivedStateKey::fromModel(
DerivedStateFamily::ArtifactTruth,
$record,
'tenant_review',
[
'user_id' => 7,
'scope' => ['tenant' => 8, 'workspace' => 12],
],
);
$right = DerivedStateKey::fromModel(
DerivedStateFamily::ArtifactTruth,
$record,
'tenant_review',
[
'scope' => ['workspace' => 12, 'tenant' => 8],
'user_id' => 7,
],
);
expect($left->fingerprint())->toBe($right->fingerprint())
->and($left->workspaceId)->toBe(12)
->and($left->tenantId)->toBe(8);
});
it('reuses cached values after the first miss', function (): void {
$store = new RequestScopedDerivedStateStore('request-a');
$key = new DerivedStateKey(DerivedStateFamily::ArtifactTruth, 'App\\Models\\TenantReview', '55', 'tenant_review');
$resolutions = 0;
$first = $store->resolve($key, function () use (&$resolutions): string {
$resolutions++;
return 'derived-result';
});
$second = $store->resolve($key, function () use (&$resolutions): string {
$resolutions++;
return 'unexpected-second-resolution';
});
$record = $store->resolutionRecord($key);
expect($first)->toBe('derived-result')
->and($second)->toBe('derived-result')
->and($resolutions)->toBe(1)
->and($record)->not->toBeNull()
->and($record['negative_result'])->toBeFalse()
->and($record['resolved_at'])->toBe(1);
});
it('reuses deterministic negative results when the family allows it', function (): void {
$store = new RequestScopedDerivedStateStore('request-b');
$key = new DerivedStateKey(DerivedStateFamily::RelatedNavigationPrimary, 'App\\Models\\Finding', '91', 'finding');
$resolutions = 0;
$first = $store->resolve($key, function () use (&$resolutions): ?string {
$resolutions++;
return null;
});
$second = $store->resolve($key, function () use (&$resolutions): string {
$resolutions++;
return 'should-not-run';
});
expect($first)->toBeNull()
->and($second)->toBeNull()
->and($resolutions)->toBe(1)
->and($store->resolutionRecord($key)['negative_result'])->toBeTrue();
});
it('keeps variants isolated for the same family and record', function (): void {
$store = new RequestScopedDerivedStateStore('request-c');
$resolutions = 0;
$tenantReviewKey = new DerivedStateKey(DerivedStateFamily::ArtifactTruth, 'App\\Models\\TenantReview', '101', 'tenant_review');
$reviewPackKey = new DerivedStateKey(DerivedStateFamily::ArtifactTruth, 'App\\Models\\TenantReview', '101', 'review_pack');
$tenantReviewValue = $store->resolve($tenantReviewKey, function () use (&$resolutions): string {
$resolutions++;
return 'tenant-review';
});
$reviewPackValue = $store->resolve($reviewPackKey, function () use (&$resolutions): string {
$resolutions++;
return 'review-pack';
});
expect($tenantReviewValue)->toBe('tenant-review')
->and($reviewPackValue)->toBe('review-pack')
->and($resolutions)->toBe(2)
->and($store->entryCount())->toBe(2);
});
it('invalidates exact keys and whole family slices', function (): void {
$store = new RequestScopedDerivedStateStore('request-d');
$tenantReviewKey = new DerivedStateKey(DerivedStateFamily::ArtifactTruth, 'App\\Models\\TenantReview', '1', 'tenant_review');
$reviewPackKey = new DerivedStateKey(DerivedStateFamily::ArtifactTruth, 'App\\Models\\ReviewPack', '9', 'review_pack');
$navigationKey = new DerivedStateKey(DerivedStateFamily::RelatedNavigationPrimary, 'App\\Models\\Finding', '1', 'finding');
$store->resolve($tenantReviewKey, static fn (): string => 'review');
$store->resolve($reviewPackKey, static fn (): string => 'pack');
$store->resolve($navigationKey, static fn (): string => 'link');
expect($store->invalidateKey($tenantReviewKey))->toBe(1)
->and($store->resolutionRecord($tenantReviewKey))->toBeNull()
->and($store->entryCount())->toBe(2)
->and($store->invalidateFamily(DerivedStateFamily::ArtifactTruth))->toBe(1)
->and($store->entryCount())->toBe(1)
->and($store->countStored(DerivedStateFamily::RelatedNavigationPrimary))->toBe(1);
});
it('supports no-reuse and fresh-resolution paths for mutation-sensitive reads', function (): void {
$store = new RequestScopedDerivedStateStore('request-e');
$key = new DerivedStateKey(DerivedStateFamily::OperationUxGuidance, 'App\\Models\\OperationRun', '44', 'surface_guidance');
$resolutions = 0;
$first = $store->resolve($key, function () use (&$resolutions): string {
$resolutions++;
return 'queued';
}, RequestScopedDerivedStateStore::FRESHNESS_NO_REUSE);
$second = $store->resolve($key, function () use (&$resolutions): string {
$resolutions++;
return 'running';
}, RequestScopedDerivedStateStore::FRESHNESS_NO_REUSE);
$store->resolve($key, static fn (): string => 'stale');
$fresh = $store->resolveFresh($key, static fn (): string => 'fresh');
expect($first)->toBe('queued')
->and($second)->toBe('running')
->and($fresh)->toBe('fresh')
->and($resolutions)->toBe(2)
->and($store->countStored(DerivedStateFamily::OperationUxGuidance, 'App\\Models\\OperationRun', '44', 'surface_guidance'))->toBe(1)
->and($store->invalidations())->toHaveCount(1);
});