Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m7s
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.
622 lines
24 KiB
PHP
622 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BackupSet;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Operations\ExecutionDenialReasonCode;
|
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
|
|
|
|
function visiblePageText(TestResponse $response): string
|
|
{
|
|
$html = (string) $response->getContent();
|
|
$html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html) ?? $html;
|
|
$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html) ?? $html;
|
|
$html = preg_replace('/\s+wire:snapshot="[^"]*"/', '', $html) ?? $html;
|
|
$html = preg_replace('/\s+wire:effects="[^"]*"/', '', $html) ?? $html;
|
|
|
|
return trim((string) preg_replace('/\s+/', ' ', strip_tags($html)));
|
|
}
|
|
|
|
function firstSectionHeadingPosition(TestResponse $response, string $heading): int|false
|
|
{
|
|
$html = (string) $response->getContent();
|
|
|
|
return preg_match('/fi-section-header-heading[^>]*>\s*'.preg_quote($heading, '/').'\s*</', $html, $matches, PREG_OFFSET_CAPTURE) === 1
|
|
? $matches[0][1]
|
|
: false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function baselineCompareGapContext(array $overrides = []): array
|
|
{
|
|
return array_replace_recursive(BaselineSubjectResolutionFixtures::compareContext([
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'WiFi-Corp-Profile',
|
|
'resolution_outcome' => 'unresolved_ambiguous_identity',
|
|
'reason_code' => 'unresolved_duplicate_candidates',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'VPN-Always-On',
|
|
'resolution_outcome' => 'unresolved_ambiguous_identity',
|
|
'reason_code' => 'unresolved_duplicate_candidates',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'Email-Exchange-Config',
|
|
'resolution_outcome' => 'unresolved_ambiguous_identity',
|
|
'reason_code' => 'unresolved_duplicate_candidates',
|
|
'operator_action_category' => 'inspect_subject_mapping',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'Deleted-Policy-ABC',
|
|
'resolution_outcome' => 'missing_local_evidence',
|
|
'reason_code' => 'missing_local_evidence',
|
|
'operator_action_category' => 'run_policy_sync_or_backup',
|
|
]),
|
|
BaselineSubjectResolutionFixtures::structuredGap([
|
|
'policy_type' => 'deviceConfiguration',
|
|
'subject_key' => 'Removed-Config-XYZ',
|
|
'resolution_outcome' => 'missing_local_evidence',
|
|
'reason_code' => 'missing_local_evidence',
|
|
'operator_action_category' => 'run_policy_sync_or_backup',
|
|
]),
|
|
]), [
|
|
'baseline_compare' => [
|
|
'subjects_total' => 50,
|
|
'reason_code' => 'compare_evidence_incomplete',
|
|
'fidelity' => 'meta',
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'effective_types' => ['deviceConfiguration'],
|
|
],
|
|
'evidence_capture' => [
|
|
'requested' => 50,
|
|
'succeeded' => 47,
|
|
'skipped' => 0,
|
|
'failed' => 3,
|
|
'throttled' => 0,
|
|
],
|
|
],
|
|
], $overrides);
|
|
}
|
|
|
|
it('renders decision-first hierarchy before main sections and technical diagnostics', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
setAdminPanelContext();
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'initiator_name' => 'Alice Example',
|
|
'summary_counts' => [
|
|
'total' => 10,
|
|
'processed' => 10,
|
|
'succeeded' => 10,
|
|
],
|
|
'context' => [
|
|
'target_scope' => [
|
|
'entra_tenant_name' => 'Contoso',
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Decision')
|
|
->assertSee('Timing')
|
|
->assertSee('Metadata')
|
|
->assertSee('Count diagnostics')
|
|
->assertSee('Contoso');
|
|
|
|
$pageText = visiblePageText($response);
|
|
|
|
$policySyncPosition = mb_strpos($pageText, 'Policy sync');
|
|
$decisionPosition = mb_strpos($pageText, 'Decision');
|
|
$timingPosition = mb_strpos($pageText, 'Timing');
|
|
$metadataPosition = mb_strpos($pageText, 'Metadata');
|
|
$relatedContextPosition = mb_strpos($pageText, 'Related context');
|
|
$countDiagnosticsPosition = mb_strpos($pageText, 'Count diagnostics');
|
|
$identityHashPosition = mb_strpos($pageText, 'Identity hash');
|
|
|
|
expect($policySyncPosition)->not->toBeFalse()
|
|
->and($decisionPosition)->not->toBeFalse()
|
|
->and($timingPosition)->not->toBeFalse()
|
|
->and($metadataPosition)->not->toBeFalse()
|
|
->and($relatedContextPosition)->not->toBeFalse()
|
|
->and($countDiagnosticsPosition)->not->toBeFalse()
|
|
->and($identityHashPosition)->not->toBeFalse()
|
|
->and($decisionPosition)->toBeLessThan($timingPosition)
|
|
->and($timingPosition)->toBeLessThan($metadataPosition)
|
|
->and($metadataPosition)->toBeLessThan($relatedContextPosition)
|
|
->and($relatedContextPosition)->toBeLessThan($countDiagnosticsPosition)
|
|
->and($countDiagnosticsPosition)->toBeLessThan($identityHashPosition);
|
|
|
|
expect((string) $response->getContent())
|
|
->toMatch('/fi-section-header-heading[^>]*>\s*Decision\s*</')
|
|
->toMatch('/fi-section-header-heading[^>]*>\s*Timing\s*</');
|
|
|
|
$response->assertDontSee('Current state')
|
|
->assertDontSee('Run summary');
|
|
});
|
|
|
|
it('keeps header navigation and related context visible for tenant-bound operation runs', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Nightly backup',
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'backup_set.update',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
'target_scope' => [
|
|
'entra_tenant_name' => 'Contoso',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Back to Operations')
|
|
->assertSee('Refresh')
|
|
->assertSee('Related context')
|
|
->assertSee(\App\Filament\Resources\BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant), false);
|
|
});
|
|
|
|
it('renders mismatch context above the enterprise detail content without blocking the page', function (): void {
|
|
$runTenant = ManagedEnvironment::factory()->create([
|
|
'name' => 'Run ManagedEnvironment',
|
|
]);
|
|
[$user, $runTenant] = createUserWithTenant(tenant: $runTenant, role: 'owner');
|
|
|
|
$currentTenant = ManagedEnvironment::factory()->create([
|
|
'name' => 'Current ManagedEnvironment',
|
|
'workspace_id' => (int) $runTenant->workspace_id,
|
|
]);
|
|
|
|
createUserWithTenant(tenant: $currentTenant, user: $user, role: 'owner');
|
|
|
|
setAdminPanelContext($currentTenant);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $runTenant->workspace_id,
|
|
'managed_environment_id' => (int) $runTenant->getKey(),
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Current environment context differs from this operation')
|
|
->assertSee('Decision')
|
|
->assertSee('Related context');
|
|
|
|
$html = (string) $response->getContent();
|
|
|
|
$bannerPosition = mb_strpos($html, 'Current environment context differs from this operation');
|
|
$decisionPosition = firstSectionHeadingPosition($response, 'Decision');
|
|
|
|
expect($bannerPosition)->not->toBeFalse()
|
|
->and($decisionPosition)->not->toBeFalse()
|
|
->and($bannerPosition)->toBeLessThan($decisionPosition);
|
|
});
|
|
|
|
it('keeps stale canonical detail aligned with lifecycle guidance instead of ordinary queued copy', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
setAdminPanelContext();
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subWeeks(2),
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Likely stale operation')
|
|
->assertSee('Decision');
|
|
|
|
$pageText = visiblePageText($response);
|
|
|
|
expect($pageText)->toContain('past its lifecycle window')
|
|
->not->toContain('No action needed yet. The operation is waiting for a worker.')
|
|
->not->toContain('Waiting for worker.');
|
|
});
|
|
|
|
it('renders explicit sparse-data fallbacks for operation runs', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
setAdminPanelContext();
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'summary_counts' => [],
|
|
'failure_summary' => [],
|
|
'context' => [],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('No target scope details were recorded for this operation.')
|
|
->assertSee('Verification report')
|
|
->assertSee('Verification report unavailable')
|
|
->assertDontSee('Counts');
|
|
});
|
|
|
|
it('renders lifecycle reconciliation diagnostics for reconciled runs', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'restore.execute',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'context' => [
|
|
'reason_code' => 'run.adapter_out_of_sync',
|
|
'reconciliation' => [
|
|
'reconciled_at' => now()->toIso8601String(),
|
|
'reason' => 'run.adapter_out_of_sync',
|
|
'reason_code' => 'run.adapter_out_of_sync',
|
|
'source' => 'adapter_reconciler',
|
|
],
|
|
],
|
|
'failure_summary' => [[
|
|
'code' => 'run.adapter_out_of_sync',
|
|
'reason_code' => 'run.adapter_out_of_sync',
|
|
'message' => 'A related restore record reached terminal truth before the operation run was updated.',
|
|
]],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Lifecycle reconciliation')
|
|
->assertSee('Automatically reconciled')
|
|
->assertSee('Reconciled by')
|
|
->assertSee('Adapter reconciler');
|
|
});
|
|
|
|
it('renders explicit reason-owner and platform-family semantics for blocked runs', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Blocked->value,
|
|
'context' => [
|
|
'reason_code' => ExecutionDenialReasonCode::MissingCapability->value,
|
|
'execution_legitimacy' => [
|
|
'reason_code' => ExecutionDenialReasonCode::MissingCapability->value,
|
|
],
|
|
],
|
|
'failure_summary' => [[
|
|
'code' => 'operation.blocked',
|
|
'reason_code' => ExecutionDenialReasonCode::MissingCapability->value,
|
|
'message' => 'Operation blocked because the initiating actor no longer has the required capability.',
|
|
]],
|
|
]);
|
|
|
|
$reasonSemantics = app(ReasonPresenter::class)->semantics(
|
|
app(ReasonPresenter::class)->forOperationRun($run, 'run_detail'),
|
|
);
|
|
|
|
expect($reasonSemantics)->not->toBeNull();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Explanation semantics')
|
|
->assertSee('Reason owner')
|
|
->assertSee($reasonSemantics['owner_label'])
|
|
->assertSee('Platform reason family')
|
|
->assertSee($reasonSemantics['family_label']);
|
|
});
|
|
|
|
it('renders evidence gap details section for baseline compare runs with gap subjects', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'summary_counts' => [
|
|
'total' => 50,
|
|
'processed' => 47,
|
|
'failed' => 3,
|
|
],
|
|
'context' => baselineCompareGapContext(),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Decision')
|
|
->assertSee('Primary next step')
|
|
->assertSee('Count diagnostics')
|
|
->assertSee('Evidence gap details')
|
|
->assertSee('Search gap details')
|
|
->assertSee('Search by reason, type, class, outcome, action, or subject key')
|
|
->assertSee('Reason')
|
|
->assertSee('Duplicate provider candidates')
|
|
->assertSee('Missing local evidence')
|
|
->assertSee('3 affected')
|
|
->assertSee('2 affected')
|
|
->assertSee('WiFi-Corp-Profile')
|
|
->assertSee('Deleted-Policy-ABC')
|
|
->assertSee('Governed subject')
|
|
->assertSee('Subject class')
|
|
->assertSee('Outcome')
|
|
->assertSee('Next action')
|
|
->assertSee('Subject key');
|
|
|
|
$pageText = visiblePageText($response);
|
|
|
|
$decisionPosition = mb_strpos($pageText, 'Decision');
|
|
$timingPosition = mb_strpos($pageText, 'Timing');
|
|
$searchGapDetailsPosition = mb_strpos($pageText, 'Search gap details');
|
|
$gapDetailsPosition = mb_strpos($pageText, 'Evidence gap details');
|
|
$countDiagnosticsPosition = mb_strpos($pageText, 'Count diagnostics');
|
|
|
|
expect($decisionPosition)->not->toBeFalse()
|
|
->and($timingPosition)->not->toBeFalse()
|
|
->and($searchGapDetailsPosition)->not->toBeFalse()
|
|
->and($gapDetailsPosition)->not->toBeFalse()
|
|
->and($countDiagnosticsPosition)->not->toBeFalse()
|
|
->and($decisionPosition)->toBeLessThan($timingPosition)
|
|
->and($timingPosition)->toBeLessThan($gapDetailsPosition)
|
|
->and($gapDetailsPosition)->toBeLessThan($searchGapDetailsPosition)
|
|
->and($gapDetailsPosition)->toBeLessThan($countDiagnosticsPosition);
|
|
});
|
|
|
|
it('renders baseline compare evidence-gap details without invoking graph during canonical run detail render', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
bindFailHardGraphClient();
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => baselineCompareGapContext(),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Evidence gap details')
|
|
->assertSee('WiFi-Corp-Profile');
|
|
});
|
|
|
|
it('distinguishes missing recorded gap detail from no-gap runs on the canonical run detail surface', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$legacyRun = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => baselineCompareGapContext([
|
|
'baseline_compare' => [
|
|
'evidence_gaps' => [
|
|
'subjects' => null,
|
|
],
|
|
],
|
|
]),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$cleanRun = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'succeeded',
|
|
'context' => baselineCompareGapContext([
|
|
'baseline_compare' => [
|
|
'reason_code' => 'no_drift_detected',
|
|
'evidence_gaps' => [
|
|
'count' => 0,
|
|
'by_reason' => [],
|
|
'subjects' => [],
|
|
],
|
|
],
|
|
]),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($legacyRun))
|
|
->assertOk()
|
|
->assertSee('Evidence gap details')
|
|
->assertSee('Detailed rows were not recorded for this run')
|
|
->assertSee('Baseline compare evidence');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($cleanRun))
|
|
->assertOk()
|
|
->assertDontSee('Evidence gap details')
|
|
->assertSee('Baseline compare evidence');
|
|
});
|
|
|
|
it('returns 404 for workspace members without tenant entitlement when evidence-gap details exist on the canonical surface', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$tenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => baselineCompareGapContext(),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
$otherTenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
]);
|
|
|
|
createUserWithTenant(tenant: $otherTenant, user: $user, role: 'owner');
|
|
app(\App\Services\Auth\ManagedEnvironmentAccessScopeResolver::class)->clearCache();
|
|
|
|
expect(app(\App\Services\Auth\ManagedEnvironmentAccessScopeResolver::class)->canAccess($user, $tenant))->toBeFalse();
|
|
|
|
setAdminPanelContext();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('renders a human-readable inventory sync coverage section before technical context', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
Filament::setTenant(null, true);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
|
'context' => [
|
|
'inventory' => [
|
|
'coverage' => InventoryCoveragePayload::buildPayload([
|
|
'conditionalAccessPolicy' => [
|
|
'status' => InventoryCoveragePayload::StatusSucceeded,
|
|
'item_count' => 1,
|
|
],
|
|
'deviceConfiguration' => [
|
|
'status' => InventoryCoveragePayload::StatusFailed,
|
|
'item_count' => 0,
|
|
'error_code' => 'graph_forbidden',
|
|
],
|
|
'roleScopeTag' => [
|
|
'status' => InventoryCoveragePayload::StatusSkipped,
|
|
'item_count' => 0,
|
|
],
|
|
], ['roleScopeTag']),
|
|
],
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(\App\Support\OperationRunLinks::tenantlessView($run))
|
|
->assertOk()
|
|
->assertSee('Inventory sync coverage')
|
|
->assertSee('Execution outcome stays separate from the per-type results below.')
|
|
->assertSee('Coverage truth below explains which types created the follow-up.')
|
|
->assertSee('deviceConfiguration')
|
|
->assertSee('roleScopeTag')
|
|
->assertSee('Review provider consent or permissions, then rerun inventory sync.')
|
|
->assertSee('Run inventory sync again with the required types selected.');
|
|
|
|
$pageText = visiblePageText($response);
|
|
|
|
$coveragePosition = mb_strpos($pageText, 'Inventory sync coverage');
|
|
$contextPosition = mb_strpos($pageText, 'Context');
|
|
|
|
expect($coveragePosition)->not->toBeFalse()
|
|
->and($contextPosition)->not->toBeFalse()
|
|
->and($coveragePosition)->toBeLessThan($contextPosition);
|
|
});
|