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); });