where('id', (int) $profile->getKey()) ->update([ 'scope_jsonb' => json_encode($scope, JSON_THROW_ON_ERROR), 'updated_at' => now(), ]); } it('previews only legacy baseline profile scope rows in mixed datasets without mutating them', function (): void { config()->set('tenantpilot.supported_policy_types', [ ['type' => 'deviceConfiguration', 'label' => 'Device Configuration'], ['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'], ]); config()->set('tenantpilot.foundation_types', [ ['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]], ]); $workspace = Workspace::factory()->create(); $legacyProfile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'Legacy preview profile', ]); $canonicalProfile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'Canonical preview profile', 'scope_jsonb' => [ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceCompliancePolicy'], 'filters' => [], ], ], ], ]); spec202ForceLegacyBaselineScope($legacyProfile, [ 'policy_types' => ['deviceConfiguration'], 'foundation_types' => [], ]); $this->artisan('tenantpilot:baseline-scope-v2:backfill', [ '--workspace' => (string) $workspace->getKey(), ]) ->expectsOutputToContain('Mode: preview') ->expectsOutputToContain('Scope surface: baseline_profiles_only') ->expectsOutputToContain('Candidate count: 1') ->expectsOutputToContain('Rewritten count: 0') ->expectsOutputToContain('Audit logged: no') ->expectsOutputToContain('Legacy preview profile') ->assertSuccessful(); $legacyProfile->refresh(); $canonicalProfile->refresh(); expect($legacyProfile->rawScopeJsonb())->toBe([ 'policy_types' => ['deviceConfiguration'], 'foundation_types' => [], ])->and($canonicalProfile->rawScopeJsonb())->toBe([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceCompliancePolicy'], 'filters' => [], ], ], ]); }); it('requires explicit write confirmation before mutating baseline profile scope rows', function (): void { $workspace = Workspace::factory()->create(); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'Legacy confirm profile', ]); spec202ForceLegacyBaselineScope($profile, [ 'policy_types' => ['deviceConfiguration'], 'foundation_types' => [], ]); $this->artisan('tenantpilot:baseline-scope-v2:backfill', [ '--workspace' => (string) $workspace->getKey(), '--write' => true, ]) ->expectsOutputToContain('Explicit write confirmation required.') ->assertFailed(); $profile->refresh(); expect($profile->rawScopeJsonb())->toBe([ 'policy_types' => ['deviceConfiguration'], 'foundation_types' => [], ]); }); it('rewrites legacy baseline profile scopes to canonical v2, logs audits, and stays idempotent on rerun', function (): void { config()->set('tenantpilot.supported_policy_types', [ ['type' => 'deviceConfiguration', 'label' => 'Device Configuration'], ['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'], ]); $workspace = Workspace::factory()->create(); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'Legacy commit profile', ]); spec202ForceLegacyBaselineScope($profile, [ 'policy_types' => ['deviceConfiguration'], 'foundation_types' => [], ]); $this->artisan('tenantpilot:baseline-scope-v2:backfill', [ '--workspace' => (string) $workspace->getKey(), '--write' => true, '--confirm-write' => true, ]) ->expectsOutputToContain('Mode: commit') ->expectsOutputToContain('Candidate count: 1') ->expectsOutputToContain('Rewritten count: 1') ->expectsOutputToContain('Audit logged: yes') ->assertSuccessful(); $profile->refresh(); expect($profile->rawScopeJsonb())->toBe([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceConfiguration'], 'filters' => [], ], ], ])->and($profile->requiresScopeSaveForward())->toBeFalse(); $this->assertDatabaseHas('audit_logs', [ 'workspace_id' => (int) $workspace->getKey(), 'action' => 'baseline_profile.scope_backfilled', 'resource_type' => 'baseline_profile', 'resource_id' => (string) $profile->getKey(), ]); $auditLog = AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('action', 'baseline_profile.scope_backfilled') ->latest('id') ->first(); expect($auditLog)->not->toBeNull(); $this->artisan('tenantpilot:baseline-scope-v2:backfill', [ '--workspace' => (string) $workspace->getKey(), ]) ->expectsOutputToContain('Candidate count: 0') ->expectsOutputToContain('No baseline profile scope rows require backfill.') ->assertSuccessful(); });