Some checks failed
Main Confidence / confidence (push) Failing after 48s
## Summary - implement the finding outcome taxonomy end-to-end with canonical resolve, close, reopen, and verification semantics - align finding UI, filters, audit metadata, review summaries, and export/read-model consumers to the shared outcome semantics - add focused Pest coverage and complete the spec artifacts for feature 231 ## Details - manual resolve is limited to the canonical `remediated` outcome - close and reopen flows now use bounded canonical reasons - trusted system clear and reopen distinguish verified-clear from verification-failed and recurrence paths - duplicate lifecycle backfill now closes findings canonically as `duplicate` - accepted-risk recording now uses the canonical `accepted_risk` reason - finding detail and list surfaces now expose terminal outcome and verification summaries - review, snapshot, and review-pack consumers now propagate the same outcome buckets ## Filament / Platform Contract - Livewire v4.0+ compatibility remains intact - provider registration is unchanged and remains in `bootstrap/providers.php` - no new globally searchable resource was introduced; `FindingResource` still has a View page and `TenantReviewResource` remains globally searchable false - lifecycle mutations still run through confirmed Filament actions with capability enforcement - no new asset family was added; the existing `filament:assets` deploy step is unchanged ## Verification - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingWorkflowServiceTest.php tests/Feature/Findings/FindingRecurrenceTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php tests/Feature/Findings/FindingRiskGovernanceProjectionTest.php` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Models/FindingResolvedTest.php tests/Unit/Findings/FindingWorkflowServiceTest.php` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewRegisterTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php` - browser smoke: `/admin/findings/my-work` -> finding detail resolve flow -> queue regression check passed ## Notes - this commit also includes the existing `.github/agents/copilot-instructions.md` workspace change that was already present in the worktree when all changes were committed Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #267
146 lines
6.7 KiB
PHP
146 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
|
use App\Filament\Resources\TenantReviewResource;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Finding;
|
|
use App\Services\Evidence\EvidenceSnapshotService;
|
|
use App\Services\Evidence\Sources\FindingsSummarySource;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\Findings\FindingOutcomeSemantics;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function seedFindingOutcomeMatrix(\App\Models\Tenant $tenant): array
|
|
{
|
|
return [
|
|
'pending_verification' => Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_RESOLVED,
|
|
'resolved_reason' => Finding::RESOLVE_REASON_REMEDIATED,
|
|
]),
|
|
'verified_cleared' => Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_RESOLVED,
|
|
'resolved_reason' => Finding::RESOLVE_REASON_NO_LONGER_DRIFTING,
|
|
]),
|
|
'closed_duplicate' => Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_CLOSED,
|
|
'closed_reason' => Finding::CLOSE_REASON_DUPLICATE,
|
|
]),
|
|
'risk_accepted' => Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
|
'closed_reason' => Finding::CLOSE_REASON_ACCEPTED_RISK,
|
|
]),
|
|
];
|
|
}
|
|
|
|
function materializeFindingOutcomeSnapshot(\App\Models\Tenant $tenant): EvidenceSnapshot
|
|
{
|
|
$payload = app(EvidenceSnapshotService::class)->buildSnapshotPayload($tenant);
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'fingerprint' => $payload['fingerprint'],
|
|
'completeness_state' => $payload['completeness'],
|
|
'summary' => $payload['summary'],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
foreach ($payload['items'] as $item) {
|
|
$snapshot->items()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'dimension_key' => $item['dimension_key'],
|
|
'state' => $item['state'],
|
|
'required' => $item['required'],
|
|
'source_kind' => $item['source_kind'],
|
|
'source_record_type' => $item['source_record_type'],
|
|
'source_record_id' => $item['source_record_id'],
|
|
'source_fingerprint' => $item['source_fingerprint'],
|
|
'measured_at' => $item['measured_at'],
|
|
'freshness_at' => $item['freshness_at'],
|
|
'summary_payload' => $item['summary_payload'],
|
|
'sort_order' => $item['sort_order'],
|
|
]);
|
|
}
|
|
|
|
return $snapshot->load('items');
|
|
}
|
|
|
|
it('summarizes canonical terminal outcomes and report buckets from findings evidence', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$findings = seedFindingOutcomeMatrix($tenant);
|
|
|
|
$summary = app(FindingsSummarySource::class)->collect($tenant)['summary_payload'] ?? [];
|
|
|
|
expect(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1)
|
|
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1)
|
|
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1)
|
|
->and(data_get($summary, 'outcome_counts.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1)
|
|
->and(data_get($summary, 'report_bucket_counts.remediation_pending_verification'))->toBe(1)
|
|
->and(data_get($summary, 'report_bucket_counts.remediation_verified'))->toBe(1)
|
|
->and(data_get($summary, 'report_bucket_counts.administrative_closure'))->toBe(1)
|
|
->and(data_get($summary, 'report_bucket_counts.accepted_risk'))->toBe(1);
|
|
|
|
$pendingEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['pending_verification']->getKey());
|
|
$verifiedEntry = collect($summary['entries'] ?? [])->firstWhere('id', (int) $findings['verified_cleared']->getKey());
|
|
|
|
expect(data_get($pendingEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION)
|
|
->and(data_get($pendingEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_PENDING)
|
|
->and(data_get($verifiedEntry, 'terminal_outcome.key'))->toBe(FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED)
|
|
->and(data_get($verifiedEntry, 'terminal_outcome.verification_state'))->toBe(FindingOutcomeSemantics::VERIFICATION_VERIFIED);
|
|
});
|
|
|
|
it('propagates finding outcome summaries into evidence snapshots tenant reviews and review packs', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
seedFindingOutcomeMatrix($tenant);
|
|
|
|
$snapshot = materializeFindingOutcomeSnapshot($tenant);
|
|
|
|
expect(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION))->toBe(1)
|
|
->and(data_get($snapshot->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED))->toBe(1)
|
|
->and(data_get($snapshot->summary, 'finding_report_buckets.accepted_risk'))->toBe(1);
|
|
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
|
|
expect(data_get($review->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_CLOSED_DUPLICATE))->toBe(1)
|
|
->and(data_get($review->summary, 'finding_report_buckets.administrative_closure'))->toBe(1);
|
|
|
|
setTenantPanelContext($tenant);
|
|
|
|
$this->actingAs($user)
|
|
->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
|
|
->assertOk()
|
|
->assertSee('Terminal outcomes:')
|
|
->assertSee('resolved pending verification')
|
|
->assertSee('verified cleared')
|
|
->assertSee('closed as duplicate')
|
|
->assertSee('risk accepted');
|
|
|
|
setAdminPanelContext();
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ReviewRegister::class)
|
|
->assertCanSeeTableRecords([$review])
|
|
->assertSee('Terminal outcomes:')
|
|
->assertSee('resolved pending verification');
|
|
|
|
$pack = app(ReviewPackService::class)->generateFromReview($review, $user, [
|
|
'include_pii' => false,
|
|
'include_operations' => false,
|
|
]);
|
|
|
|
expect(data_get($pack->summary, 'finding_outcomes.'.FindingOutcomeSemantics::OUTCOME_RISK_ACCEPTED))->toBe(1)
|
|
->and(data_get($pack->summary, 'finding_report_buckets.accepted_risk'))->toBe(1);
|
|
});
|