create(); $otherTenant = Tenant::factory()->create(); [$user] = createUserWithTenant($otherTenant, role: 'readonly'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'provider.connection.check', 'status' => 'completed', 'outcome' => 'failed', 'context' => [ 'verification_report' => json_decode( (string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')), true, 512, JSON_THROW_ON_ERROR, ), ], ]); expect(fn () => app(VerificationCheckAcknowledgementService::class)->acknowledge( tenant: $tenant, run: $run, checkKey: 'provider_connection.token_acquisition', ackReason: 'Known issue', expiresAt: null, actor: $user, ))->toThrow(NotFoundHttpException::class); }); it('returns 403 for members without tenant_verification.acknowledge on verification check acknowledgement', function (): void { [$user, $tenant] = createUserWithTenant(role: 'operator'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'provider.connection.check', 'status' => 'completed', 'outcome' => 'failed', 'context' => [ 'verification_report' => json_decode( (string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')), true, 512, JSON_THROW_ON_ERROR, ), ], ]); expect(fn () => app(VerificationCheckAcknowledgementService::class)->acknowledge( tenant: $tenant, run: $run, checkKey: 'provider_connection.token_acquisition', ackReason: 'Known issue', expiresAt: null, actor: $user, ))->toThrow(AuthorizationException::class); }); it('acknowledges a failing check (with optional expiry) and writes a minimal audit log (no ack_reason)', function (): void { [$user, $tenant] = createUserWithTenant(role: 'manager'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'provider.connection.check', 'status' => 'completed', 'outcome' => 'failed', 'context' => [ 'verification_report' => json_decode( (string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')), true, 512, JSON_THROW_ON_ERROR, ), ], ]); $reportBefore = $run->context['verification_report'] ?? null; $reportBefore = is_array($reportBefore) ? $reportBefore : []; $summaryBefore = is_array($reportBefore['summary'] ?? null) ? $reportBefore['summary'] : []; $countsBefore = is_array($summaryBefore['counts'] ?? null) ? $summaryBefore['counts'] : []; $checksBefore = is_array($reportBefore['checks'] ?? null) ? $reportBefore['checks'] : []; $checksBeforeByKey = collect($checksBefore) ->filter(fn ($check): bool => is_array($check) && is_string($check['key'] ?? null)) ->mapWithKeys(fn (array $check): array => [ (string) $check['key'] => [ 'status' => $check['status'] ?? null, 'blocking' => $check['blocking'] ?? null, 'severity' => $check['severity'] ?? null, 'reason_code' => $check['reason_code'] ?? null, ], ]) ->all(); $fingerprintBefore = VerificationReportFingerprint::forReport($reportBefore); $expiresAt = now()->addDay()->toISOString(); $ack = app(VerificationCheckAcknowledgementService::class)->acknowledge( tenant: $tenant, run: $run, checkKey: 'provider_connection.token_acquisition', ackReason: 'Known issue', expiresAt: $expiresAt, actor: $user, ); expect($ack->operation_run_id)->toBe((int) $run->getKey()); expect($ack->check_key)->toBe('provider_connection.token_acquisition'); expect($ack->ack_reason)->toBe('Known issue'); expect($ack->expires_at)->not->toBeNull(); $run->refresh(); $reportAfter = $run->context['verification_report'] ?? null; $reportAfter = is_array($reportAfter) ? $reportAfter : []; $summaryAfter = is_array($reportAfter['summary'] ?? null) ? $reportAfter['summary'] : []; $countsAfter = is_array($summaryAfter['counts'] ?? null) ? $summaryAfter['counts'] : []; expect($summaryAfter['overall'] ?? null)->toBe($summaryBefore['overall'] ?? null); expect($countsAfter)->toBe($countsBefore); $checksAfter = is_array($reportAfter['checks'] ?? null) ? $reportAfter['checks'] : []; $checksAfterByKey = collect($checksAfter) ->filter(fn ($check): bool => is_array($check) && is_string($check['key'] ?? null)) ->mapWithKeys(fn (array $check): array => [ (string) $check['key'] => [ 'status' => $check['status'] ?? null, 'blocking' => $check['blocking'] ?? null, 'severity' => $check['severity'] ?? null, 'reason_code' => $check['reason_code'] ?? null, ], ]) ->all(); expect($checksAfterByKey)->toBe($checksBeforeByKey); $fingerprintAfter = VerificationReportFingerprint::forReport($reportAfter); expect($fingerprintAfter)->toBe($fingerprintBefore); $audit = AuditLog::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('action', AuditActionId::VerificationCheckAcknowledged->value) ->latest('id') ->first(); expect($audit)->not->toBeNull(); expect($audit?->metadata)->toMatchArray([ 'tenant_id' => (int) $tenant->getKey(), 'operation_run_id' => (int) $run->getKey(), 'report_id' => (int) $run->getKey(), 'flow' => 'provider.connection.check', 'check_key' => 'provider_connection.token_acquisition', 'reason_code' => 'authentication_failed', ]); $metadata = $audit?->metadata ?? []; expect($metadata)->not->toHaveKey('ack_reason'); expect(json_encode($metadata))->not->toContain('Known issue'); });