TenantAtlas/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php
ahmido 421261a517
Some checks failed
Main Confidence / confidence (push) Failing after 48s
feat: implement finding outcome taxonomy (#267)
## 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
2026-04-23 07:29:05 +00:00

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