fix: resolve post-suite state regressions

This commit is contained in:
Ahmed Darrazi 2026-03-29 23:12:40 +02:00
parent d39d6f0982
commit 4c174c717b
4 changed files with 37 additions and 18 deletions

View File

@ -131,7 +131,7 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions support filtered findings operations (legacy acknowledge-all-matching remains until bulk workflow migration).')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions are grouped under "More".')
->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary workflow actions are grouped under "More"; the only inline row action is the related-record drill-down.')
->satisfy(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are grouped under "More".')
->exempt(ActionSurfaceSlot::ListEmptyState, 'Findings are generated by drift detection and intentionally have no create CTA.')
->satisfy(ActionSurfaceSlot::DetailHeader, 'View page exposes capability-gated workflow actions for finding lifecycle management.');
@ -1643,6 +1643,7 @@ public static function reopenAction(): Actions\Action
*/
private static function runWorkflowMutation(Finding $record, string $successTitle, callable $callback): void
{
$pageRecord = $record;
$record = static::resolveProtectedFindingRecordOrFail($record);
$tenant = static::resolveTenantContextForCurrentPanel();
$user = auth()->user();
@ -1671,6 +1672,8 @@ private static function runWorkflowMutation(Finding $record, string $successTitl
try {
$callback($record, $tenant, $user);
$pageRecord->refresh();
} catch (InvalidArgumentException $e) {
Notification::make()
->title('Workflow action failed')

View File

@ -108,7 +108,9 @@ private function getMembership(User $user, Tenant $tenant): ?array
/**
* Prime membership cache for a set of tenants in one query.
*
* Used to avoid N+1 queries for bulk selection authorization.
* Used to avoid N+1 queries for bulk selection authorization while still
* reflecting membership changes that may have happened earlier in the same
* request or test process.
*
* @param array<int, int|string> $tenantIds
*/
@ -120,26 +122,14 @@ public function primeMemberships(User $user, array $tenantIds): void
return;
}
$missingTenantIds = [];
foreach ($tenantIds as $tenantId) {
$cacheKey = "membership_{$user->id}_{$tenantId}";
if (! array_key_exists($cacheKey, $this->resolvedMemberships)) {
$missingTenantIds[] = $tenantId;
}
}
if ($missingTenantIds === []) {
return;
}
$memberships = TenantMembership::query()
->where('user_id', $user->id)
->whereIn('tenant_id', $missingTenantIds)
->whereIn('tenant_id', $tenantIds)
->get(['tenant_id', 'role', 'source', 'source_ref']);
$byTenantId = $memberships->keyBy('tenant_id');
foreach ($missingTenantIds as $tenantId) {
foreach ($tenantIds as $tenantId) {
$cacheKey = "membership_{$user->id}_{$tenantId}";
$membership = $byTenantId->get($tenantId);
$this->resolvedMemberships[$cacheKey] = $membership?->toArray();

View File

@ -5,12 +5,15 @@
namespace App\Services\TenantReviews;
use App\Models\EvidenceSnapshot;
use App\Models\ReviewPack;
use App\Models\Tenant;
use App\Models\TenantReview;
use App\Models\User;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use App\Support\TenantReviewStatus;
use App\Support\Ui\DerivedState\DerivedStateFamily;
use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
@ -20,6 +23,7 @@ public function __construct(
private readonly TenantReviewReadinessGate $readinessGate,
private readonly TenantReviewService $reviewService,
private readonly WorkspaceAuditLogger $auditLogger,
private readonly RequestScopedDerivedStateStore $derivedStateStore,
) {}
public function publish(TenantReview $review, User $user): TenantReview
@ -64,6 +68,8 @@ public function publish(TenantReview $review, User $user): TenantReview
tenant: $tenant,
);
$this->invalidateArtifactTruthCache($review);
return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']);
}
@ -104,6 +110,8 @@ public function archive(TenantReview $review, User $user): TenantReview
tenant: $tenant,
);
$this->invalidateArtifactTruthCache($review);
return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']);
}
@ -126,7 +134,7 @@ public function createNextReview(TenantReview $review, User $user, ?EvidenceSnap
throw new InvalidArgumentException('An eligible evidence snapshot is required to create the next review.');
}
return DB::transaction(function () use ($review, $user, $snapshot, $tenant): TenantReview {
$nextReview = DB::transaction(function () use ($review, $user, $snapshot, $tenant): TenantReview {
$nextReview = $this->reviewService->create($tenant, $snapshot, $user);
if ((int) $nextReview->getKey() !== (int) $review->getKey()) {
@ -156,5 +164,23 @@ public function createNextReview(TenantReview $review, User $user, ?EvidenceSnap
return $nextReview->refresh()->load(['tenant', 'evidenceSnapshot', 'sections', 'operationRun', 'initiator', 'publisher']);
});
$this->invalidateArtifactTruthCache($review);
$this->invalidateArtifactTruthCache($nextReview);
return $nextReview;
}
private function invalidateArtifactTruthCache(TenantReview $review): void
{
$this->derivedStateStore->invalidateModel(DerivedStateFamily::ArtifactTruth, $review, 'tenant_review');
$review->loadMissing('currentExportReviewPack');
$pack = $review->currentExportReviewPack;
if ($pack instanceof ReviewPack) {
$this->derivedStateStore->invalidateModel(DerivedStateFamily::ArtifactTruth, $pack, 'review_pack');
}
}
}

View File

@ -58,7 +58,7 @@ public function resolve(ReferenceDescriptor $descriptor): ResolvedReference
secondaryLabel: 'Backup set #'.$backupSet->getKey(),
linkTarget: new ReferenceLinkTarget(
targetKind: ReferenceClass::BackupSet->value,
url: BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $backupSet->tenant),
url: BackupSetResource::getUrl('view', ['record' => $backupSet], panel: 'tenant', tenant: $backupSet->tenant),
actionLabel: 'View backup set',
contextBadge: 'Tenant',
),