## 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
169 lines
6.0 KiB
PHP
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);
|
|
});
|