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.
447 lines
19 KiB
PHP
447 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\OperationRun;
|
|
use App\Services\Baselines\BaselineCaptureService;
|
|
use App\Services\Baselines\BaselineCompareService;
|
|
use App\Support\Baselines\BaselineCompareReasonCode;
|
|
use App\Support\Baselines\BaselineCompareStats;
|
|
use App\Support\Baselines\BaselineReasonCodes;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Features\SupportTesting\Testable;
|
|
use Livewire\Livewire;
|
|
|
|
function visibleLivewireText(Testable $component): string
|
|
{
|
|
$html = $component->html();
|
|
$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)));
|
|
}
|
|
|
|
it('shows run outcome and baseline artifact truth as separate facts on the run detail page', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->incomplete(BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED)->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_capture',
|
|
'status' => 'completed',
|
|
'outcome' => 'failed',
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'result' => [
|
|
'snapshot_id' => (int) $snapshot->getKey(),
|
|
'snapshot_lifecycle' => 'incomplete',
|
|
],
|
|
'reason_code' => BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED,
|
|
],
|
|
'failure_summary' => [
|
|
['reason_code' => BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED, 'message' => 'Snapshot capture stopped after persistence failed.'],
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
$explanation = $truth->operatorExplanation;
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
$component = Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('Decision')
|
|
->assertSee('Outcome')
|
|
->assertSee('Artifact truth')
|
|
->assertSee('Execution failed')
|
|
->assertSee('The baseline capture finished without a usable snapshot.')
|
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
|
->assertSee('Artifact not usable')
|
|
->assertSee('Primary next step')
|
|
->assertSee('Artifact truth details')
|
|
->assertSee('Inspect the related capture diagnostics before using this snapshot')
|
|
->assertDontSee('Artifact next step');
|
|
|
|
$pageText = visibleLivewireText($component);
|
|
|
|
expect(mb_substr_count($pageText, 'Primary next step'))->toBe(1)
|
|
->and(mb_substr_count($pageText, 'Inspect the related capture diagnostics before using this snapshot'))->toBe(1)
|
|
->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'));
|
|
});
|
|
|
|
it('shows the shared blocked-inventory explanation for baseline capture runs without a usable snapshot', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_capture',
|
|
'status' => 'completed',
|
|
'outcome' => 'blocked',
|
|
'context' => [
|
|
'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_FAILED,
|
|
'baseline_capture' => [
|
|
'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_FAILED,
|
|
'current_baseline_changed' => false,
|
|
],
|
|
],
|
|
'failure_summary' => [
|
|
['reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_FAILED, 'message' => 'Capture blocked because the latest inventory sync failed.'],
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
$explanation = $truth->operatorExplanation;
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('Blocked by prerequisite')
|
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
|
->assertSee('Latest inventory sync failed')
|
|
->assertSee($explanation?->nextActionText ?? '');
|
|
});
|
|
|
|
it('shows operator explanation facts for baseline compare runs with nested compare reason context', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => [
|
|
'baseline_compare' => [
|
|
'reason_code' => 'compare_evidence_incomplete',
|
|
'coverage' => [
|
|
'proof' => false,
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 4,
|
|
],
|
|
],
|
|
],
|
|
'summary_counts' => [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
'errors_recorded' => 0,
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
$explanation = $truth->operatorExplanation;
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
$component = Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('Decision')
|
|
->assertSee('Artifact truth')
|
|
->assertSee('Result meaning')
|
|
->assertSee('Result trust')
|
|
->assertSee('Primary next step')
|
|
->assertSee('Artifact truth details')
|
|
->assertSee('The compare finished, but no decision-grade result is available yet.')
|
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
|
->assertSee($explanation?->nextActionText ?? '')
|
|
->assertSee('Resume or rerun evidence capture before relying on this compare result.')
|
|
->assertDontSee('Artifact next step');
|
|
|
|
$pageText = visibleLivewireText($component);
|
|
|
|
expect(mb_substr_count($pageText, 'Primary next step'))->toBe(1)
|
|
->and(mb_substr_count($pageText, 'Resume or rerun evidence capture before relying on this compare result.'))->toBe(1)
|
|
->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'));
|
|
});
|
|
|
|
it('shows strategy diagnostics and operator-safe failure meaning for strategy-owned compare failures', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => [
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::StrategyFailed->value,
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['conditionalAccessPolicy'],
|
|
'covered_types' => ['conditionalAccessPolicy'],
|
|
'uncovered_types' => [],
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => [
|
|
'compare_failed' => 1,
|
|
],
|
|
],
|
|
'strategy' => [
|
|
'key' => 'intune_policy',
|
|
'selection_state' => 'supported',
|
|
'operator_reason' => 'Compare strategy resolved successfully.',
|
|
'execution_diagnostics' => [
|
|
'failed' => true,
|
|
'exception_class' => RuntimeException::class,
|
|
],
|
|
'state_counts' => [
|
|
'failed' => 2,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'summary_counts' => [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
'errors_recorded' => 1,
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
$explanation = $truth->operatorExplanation;
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('The compare finished, but a compare strategy failure kept the result incomplete.')
|
|
->assertSee($explanation?->nextActionText ?? '')
|
|
->assertSee('Compare strategy')
|
|
->assertSee('Intune Policy')
|
|
->assertSee('Strategy selection')
|
|
->assertSee('Supported')
|
|
->assertSee('Strategy subject states')
|
|
->assertSee('Failed 2')
|
|
->assertSee('Baseline compare evidence')
|
|
->assertSee('RuntimeException');
|
|
});
|
|
|
|
it('deduplicates repeated artifact truth explanation text for follow-up runs without a usable artifact', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'summary_counts' => [
|
|
'total' => 50,
|
|
'processed' => 47,
|
|
'failed' => 3,
|
|
],
|
|
'context' => [],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
$component = Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('Decision')
|
|
->assertSee('Artifact truth details')
|
|
->assertSee('The operation finished without a usable artifact result.');
|
|
|
|
$pageText = visibleLivewireText($component);
|
|
|
|
expect(mb_substr_count($pageText, 'The operation finished without a usable artifact result.'))->toBe(1)
|
|
->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'));
|
|
});
|
|
|
|
it('keeps the compact tenant summary at least as cautious as the canonical run detail for suppressed compare results', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'baseline_compare',
|
|
'status' => 'completed',
|
|
'outcome' => 'partially_succeeded',
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::CoverageUnproven->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration', 'deviceCompliancePolicy'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => ['deviceCompliancePolicy'],
|
|
'proof' => false,
|
|
],
|
|
],
|
|
],
|
|
'summary_counts' => [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
'errors_recorded' => 2,
|
|
],
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$summary = BaselineCompareStats::forTenant($tenant)->summaryAssessment();
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
$explanation = $truth->operatorExplanation;
|
|
|
|
expect($summary->stateFamily)->not->toBe('positive')
|
|
->and($summary->evaluationResult)->toBe('suppressed_result')
|
|
->and($summary->headline)->toBe('The last compare finished, but normal result output was suppressed.')
|
|
->and($explanation?->evaluationResult)->toBe('suppressed_result');
|
|
|
|
Filament::setTenant(null, true);
|
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
|
->assertSee('The compare finished, but no decision-grade result is available yet.')
|
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
|
->assertDontSee('No confirmed drift in the latest baseline compare.');
|
|
});
|
|
|
|
it('records canonical effective scope and compatibility projection for baseline capture runs', function (): void {
|
|
Queue::fake();
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
|
]);
|
|
$inventorySyncRun = createInventorySyncOperationRunWithCoverage($tenant, [
|
|
'deviceConfiguration' => 'succeeded',
|
|
]);
|
|
|
|
$result = app(BaselineCaptureService::class)->startCapture($profile, $tenant, $user);
|
|
|
|
expect($result['ok'])->toBeTrue();
|
|
|
|
$run = $result['run'];
|
|
$effectiveScope = is_array(data_get($run->context, 'effective_scope')) ? data_get($run->context, 'effective_scope') : [];
|
|
|
|
expect(data_get($effectiveScope, 'canonical_scope.version'))->toBe(2)
|
|
->and(data_get($effectiveScope, 'canonical_scope.entries.0.domain_key'))->toBe('intune')
|
|
->and(data_get($effectiveScope, 'canonical_scope.entries.0.subject_class'))->toBe('policy')
|
|
->and(data_get($effectiveScope, 'canonical_scope.entries.0.subject_type_keys'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'legacy_projection.policy_types'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'legacy_projection.foundation_types'))->toBe([])
|
|
->and(data_get($effectiveScope, 'selected_type_keys'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'allowed_type_keys'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'unsupported_type_keys'))->toBe([])
|
|
->and(data_get($run->context, 'baseline_capture.inventory_sync_run_id'))->toBe((int) $inventorySyncRun->getKey())
|
|
->and(data_get($run->context, 'baseline_capture.eligibility.phase'))->toBe('preflight')
|
|
->and(data_get($run->context, 'baseline_capture.eligibility.ok'))->toBeTrue();
|
|
});
|
|
|
|
it('normalizes legacy compare assignment overrides into canonical effective scope without rewriting the override row', function (): void {
|
|
Queue::fake();
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'scope_jsonb' => ['policy_types' => ['deviceConfiguration', 'deviceCompliancePolicy'], 'foundation_types' => []],
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
$assignment = BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'override_scope_jsonb' => null,
|
|
]);
|
|
|
|
DB::table('baseline_tenant_assignments')
|
|
->where('id', (int) $assignment->getKey())
|
|
->update([
|
|
'override_scope_jsonb' => json_encode([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
], JSON_THROW_ON_ERROR),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$result = app(BaselineCompareService::class)->startCompare($tenant, $user);
|
|
|
|
expect($result['ok'])->toBeTrue();
|
|
|
|
$run = $result['run'];
|
|
$effectiveScope = is_array(data_get($run->context, 'effective_scope')) ? data_get($run->context, 'effective_scope') : [];
|
|
$rawOverride = DB::table('baseline_tenant_assignments')
|
|
->where('id', (int) $assignment->getKey())
|
|
->value('override_scope_jsonb');
|
|
|
|
expect(data_get($effectiveScope, 'canonical_scope.version'))->toBe(2)
|
|
->and(data_get($effectiveScope, 'canonical_scope.entries.0.subject_type_keys'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'legacy_projection.policy_types'))->toBe(['deviceConfiguration'])
|
|
->and(data_get($effectiveScope, 'selected_type_keys'))->toBe(['deviceConfiguration'])
|
|
->and(json_decode((string) $rawOverride, true, flags: JSON_THROW_ON_ERROR))->toBe([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
]);
|
|
});
|