instance(GraphClientInterface::class, spec423FailingGraphClient()); $details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource, $environment, $user); $summary = $details['typed_render_summary'] ?? null; $encoded = json_encode($summary, JSON_THROW_ON_ERROR); expect($summary)->toBeArray() ->and($summary['resource_type'])->toBe($resourceType) ->and($encoded)->toContain($expectedText) ->and($encoded)->toContain('readiness_requires_manual_review') ->and($encoded)->not->toContain('raw_payload') ->and($encoded)->not->toContain('source_endpoint') ->and($encoded)->not->toContain('spec423-feature-secret') ->and($encoded)->not->toContain('spec423-feature-content') ->and($summary['compare_summary']['status'])->toBe('Manual review required') ->and($summary['compare_summary']['changed'])->toBeTrue() ->and(collect($summary['compare_summary']['changes'])->pluck('label'))->toContain($expectedChange); })->with([ 'retention policy' => [ 'retentionCompliancePolicy', [ 'DisplayName' => 'Spec423 Feature Retention Policy', 'RetentionDuration' => 5, 'RetentionDurationUnit' => 'Years', 'DispositionAction' => 'Keep', 'IncludedLocations' => ['Exchange'], ], [ 'DisplayName' => 'Spec423 Feature Retention Policy', 'RetentionDuration' => 7, 'RetentionDurationUnit' => 'Years', 'DispositionAction' => 'Delete', 'IncludedLocations' => ['Exchange'], 'clientSecret' => 'spec423-feature-secret', ], 'Retention compliance policy', '7 Years', 'Retention Duration', ], 'label policy' => [ 'labelPolicy', [ 'DisplayName' => 'Spec423 Feature Label Policy', 'PublishedLabels' => [['displayName' => 'General']], 'Mandatory' => false, ], [ 'DisplayName' => 'Spec423 Feature Label Policy', 'PublishedLabels' => [['displayName' => 'Highly Confidential']], 'Mandatory' => true, ], 'Label policy', 'Highly Confidential', 'Labeling Published Labels', ], 'dlp policy' => [ 'dlpCompliancePolicy', [ 'DisplayName' => 'Spec423 Feature DLP Policy', 'Mode' => 'Audit', 'Locations' => ['Exchange'], 'Rules' => [['Name' => 'Rule', 'Actions' => ['NotifyUser']]], ], [ 'DisplayName' => 'Spec423 Feature DLP Policy', 'Mode' => 'Enforce', 'Locations' => ['Exchange'], 'Rules' => [['Name' => 'Rule', 'Actions' => ['BlockAccess'], 'DlpIncidentContent' => 'spec423-feature-content']], ], 'DLP compliance policy', 'Enforce', 'Mode', ], ]); it('Spec423 does not render typed summaries for non-renderable latest evidence', function (): void { [$user, $environment, $resource, $latestEvidence] = spec423FeatureEvidencePair( 'retentionCompliancePolicy', ['DisplayName' => 'Spec423 Non Renderable Retention', 'RetentionDuration' => 5], ['DisplayName' => 'Spec423 Non Renderable Retention', 'RetentionDuration' => 7], ); $latestEvidence->forceFill(['coverage_level' => CoverageLevel::ContentBacked->value])->save(); $resource->unsetRelation('latestEvidence'); $details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->fresh(), $environment, $user); expect($details['typed_render_summary'] ?? null)->toBeNull(); }); it('Spec423 requires latest evidence to belong to the same provider connection as the resource', function (): void { [$user, $environment, $resource, $latestEvidence] = spec423FeatureEvidencePair( 'labelPolicy', ['DisplayName' => 'Spec423 Provider Labels', 'Mandatory' => false], ['DisplayName' => 'Spec423 Provider Labels', 'Mandatory' => true], ); $foreignConnection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $latestEvidence->forceFill(['provider_connection_id' => (int) $foreignConnection->getKey()])->save(); $resource->unsetRelation('latestEvidence'); $details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->fresh(), $environment, $user); expect($details['typed_render_summary'] ?? null)->toBeNull(); }); it('Spec423 promotes mandatory Security and Compliance content-backed evidence rows to renderable coverage', function (string $canonicalType, array $payload): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $resourceType = spec423FeatureResourceType($canonicalType); $resource = TenantConfigurationResource::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), 'resource_type_id' => (int) $resourceType->getKey(), 'canonical_type' => $canonicalType, 'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-promotion', 'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value, 'source_resource_id' => 'spec423-promotion', 'source_display_name' => 'Spec423 promotion '.$canonicalType, 'source_class' => SourceClass::Tcm->value, 'latest_evidence_state' => EvidenceState::ContentBacked->value, 'latest_identity_state' => IdentityState::Stable->value, 'latest_claim_state' => ClaimState::InternalOnly->value, ]); $run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, ]); $evidence = app(CoverageEvidenceWriter::class)->append( resource: $resource, resourceType: $resourceType, providerConnection: $connection, operationRun: $run, decision: new CoverageSourceContractDecision( canonicalType: $canonicalType, outcome: CaptureOutcome::Captured, contractKey: 'spec423.synthetic.'.$canonicalType, sourceEndpoint: '/spec423/synthetic/'.$canonicalType, ), rawPayload: $payload, normalizedPayload: $payload, payloadHash: hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)), ); expect($evidence)->toBeInstanceOf(TenantConfigurationResourceEvidence::class) ->and($evidence->coverage_level)->toBe(CoverageLevel::Renderable) ->and($resource->fresh()->latest_evidence_state)->toBe(EvidenceState::ContentBacked); })->with([ 'retentionCompliancePolicy' => ['retentionCompliancePolicy', ['DisplayName' => 'Retention', 'RetentionDuration' => 7, 'DispositionAction' => 'Delete', 'clientSecret' => 'spec423-promotion-secret']], 'labelPolicy' => ['labelPolicy', ['DisplayName' => 'Labels', 'PublishedLabels' => [['displayName' => 'Highly Confidential']]]], 'dlpCompliancePolicy' => ['dlpCompliancePolicy', ['DisplayName' => 'DLP', 'Mode' => 'Enforce', 'Rules' => [['Name' => 'Rule', 'Actions' => ['BlockAccess']]]]], ]); it('Spec423 keeps unknown nested material fields renderable with manual-review readiness', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $canonicalType = 'dlpCompliancePolicy'; $resourceType = spec423FeatureResourceType($canonicalType); $payload = [ 'DisplayName' => 'Spec423 Nested DLP', 'Mode' => 'Enforce', 'Rules' => [[ 'Name' => 'Nested condition rule', 'Actions' => ['BlockAccess'], 'Conditions' => ['SensitiveInfoTypes' => ['Spec423 Credit Card Detector']], ]], ]; $resource = TenantConfigurationResource::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), 'resource_type_id' => (int) $resourceType->getKey(), 'canonical_type' => $canonicalType, 'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-nested', 'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value, 'source_resource_id' => 'spec423-nested', 'source_display_name' => 'Spec423 Nested DLP', 'source_class' => SourceClass::Tcm->value, 'latest_evidence_state' => EvidenceState::ContentBacked->value, 'latest_identity_state' => IdentityState::Stable->value, 'latest_claim_state' => ClaimState::InternalOnly->value, ]); $run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, ]); $evidence = app(CoverageEvidenceWriter::class)->append( resource: $resource, resourceType: $resourceType, providerConnection: $connection, operationRun: $run, decision: new CoverageSourceContractDecision( canonicalType: $canonicalType, outcome: CaptureOutcome::Captured, contractKey: 'spec423.synthetic.'.$canonicalType, sourceEndpoint: '/spec423/synthetic/'.$canonicalType, ), rawPayload: $payload, normalizedPayload: $payload, payloadHash: hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)), ); $details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->refresh(), $environment, $user); $summary = $details['typed_render_summary'] ?? null; $encoded = json_encode($summary, JSON_THROW_ON_ERROR); expect($evidence->coverage_level)->toBe(CoverageLevel::Renderable) ->and($summary)->toBeArray() ->and(data_get($summary, 'readiness.state'))->toBe('readiness_requires_manual_review') ->and(data_get($summary, 'readiness.label'))->toBe('Manual review required') ->and($summary['unsupported_fields'])->toContain('Rules.0.Conditions') ->and($encoded)->not->toContain('Spec423 Credit Card Detector'); }); it('Spec423 leaves optional Security and Compliance types unpromoted without bounded evidence', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); $resourceTypes = TenantConfigurationResourceType::query() ->whereIn('canonical_type', [ 'autoSensitivityLabelPolicy', 'protectionAlert', 'complianceTag', ]) ->get() ->keyBy('canonical_type'); expect($resourceTypes)->toHaveCount(3); foreach ($resourceTypes as $resourceType) { expect($resourceType->default_coverage_level)->toBe(CoverageLevel::Detected) ->and($resourceType->default_evidence_state)->toBe(EvidenceState::NotCaptured) ->and($resourceType->allows_certified_claims)->toBeFalse() ->and($resourceType->restore_tier->value)->toBe('not_restorable'); } }); it('Spec423 keeps Security and Compliance support separate from restore, legal, certification, customer output, and tenant ownership', function (): void { $paths = [ 'apps/platform/app/Services/TenantConfiguration/SecurityComplianceComparablePayloadNormalizer.php', 'apps/platform/app/Services/TenantConfiguration/SecurityComplianceCoverageComparator.php', 'apps/platform/app/Services/TenantConfiguration/SecurityComplianceRenderableSummaryBuilder.php', 'apps/platform/app/Services/TenantConfiguration/SecurityComplianceReadinessEvaluator.php', 'apps/platform/app/Services/TenantConfiguration/ClaimGuard.php', ]; $content = collect($paths) ->map(fn (string $path): string => file_exists(repo_path($path)) ? (file_get_contents(repo_path($path)) ?: '') : '') ->implode("\n"); expect($content) ->not->toContain('GraphClientInterface') ->not->toContain('Http::') ->not->toContain('tenant_id') ->not->toContain('restore-ready') ->not->toContain('certification-ready') ->not->toContain('legal-ready') ->not->toContain('customer-ready') ->not->toContain('ReviewPack') ->not->toContain('namespace App\\Services\\TenantConfiguration\\SecurityCompliance'); }); /** * @return array{0: mixed, 1: mixed, 2: TenantConfigurationResource, 3: TenantConfigurationResourceEvidence} */ function spec423FeatureEvidencePair(string $canonicalType, array $previousPayload, array $latestPayload): array { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $resourceType = spec423FeatureResourceType($canonicalType); $displayName = (string) ($latestPayload['DisplayName'] ?? $latestPayload['displayName'] ?? 'Spec423 Feature Resource'); $resource = TenantConfigurationResource::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), 'resource_type_id' => (int) $resourceType->getKey(), 'canonical_type' => $canonicalType, 'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-feature', 'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value, 'source_resource_id' => 'spec423-feature', 'source_display_name' => $displayName, 'source_class' => SourceClass::Tcm->value, 'source_metadata' => [ 'source_contract_key' => 'spec423.synthetic.'.$canonicalType, 'source_endpoint' => '/spec423/synthetic/'.$canonicalType, 'source_version' => 'v1.0', 'registry_source_class' => SourceClass::Tcm->value, 'registry_support_state' => 'out_of_scope', ], 'latest_evidence_state' => EvidenceState::ContentBacked->value, 'latest_identity_state' => IdentityState::Stable->value, 'latest_claim_state' => ClaimState::InternalOnly->value, 'latest_captured_at' => now(), ]); $previousRun = spec423FeatureRun($user, $environment, $connection, $canonicalType, minutesAgo: 5); $latestRun = spec423FeatureRun($user, $environment, $connection, $canonicalType); TenantConfigurationResourceEvidence::factory()->create([ 'resource_id' => (int) $resource->getKey(), 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), 'resource_type_id' => (int) $resourceType->getKey(), 'operation_run_id' => (int) $previousRun->getKey(), 'source_contract_key' => 'spec423.synthetic.'.$canonicalType, 'source_endpoint' => '/spec423/synthetic/'.$canonicalType, 'source_version' => 'v1.0', 'raw_payload' => ['id' => 'spec423-feature'], 'normalized_payload' => $previousPayload, 'payload_hash' => hash('sha256', json_encode($previousPayload, JSON_THROW_ON_ERROR)), 'evidence_state' => EvidenceState::ContentBacked->value, 'coverage_level' => CoverageLevel::Comparable->value, 'capture_outcome' => CaptureOutcome::Captured->value, 'captured_at' => now()->subMinutes(5), ]); $latestEvidence = TenantConfigurationResourceEvidence::factory()->create([ 'resource_id' => (int) $resource->getKey(), 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), 'resource_type_id' => (int) $resourceType->getKey(), 'operation_run_id' => (int) $latestRun->getKey(), 'source_contract_key' => 'spec423.synthetic.'.$canonicalType, 'source_endpoint' => '/spec423/synthetic/'.$canonicalType, 'source_version' => 'v1.0', 'raw_payload' => ['id' => 'spec423-feature', 'secret' => 'spec423-feature-secret'], 'normalized_payload' => $latestPayload, 'payload_hash' => hash('sha256', json_encode($latestPayload, JSON_THROW_ON_ERROR)), 'evidence_state' => EvidenceState::ContentBacked->value, 'coverage_level' => CoverageLevel::Renderable->value, 'capture_outcome' => CaptureOutcome::Captured->value, 'captured_at' => now(), ]); $resource->forceFill([ 'latest_evidence_id' => (int) $latestEvidence->getKey(), 'latest_payload_hash' => (string) $latestEvidence->payload_hash, ])->save(); return [$user, $environment, $resource->refresh(), $latestEvidence]; } function spec423FeatureResourceType(string $canonicalType): TenantConfigurationResourceType { return TenantConfigurationResourceType::query() ->where('canonical_type', $canonicalType) ->where('source_class', SourceClass::Tcm->value) ->firstOrFail(); } function spec423FeatureRun($user, $environment, ProviderConnection $connection, string $canonicalType, int $minutesAgo = 0): OperationRun { $timestamp = $minutesAgo > 0 ? now()->subMinutes($minutesAgo) : now(); return OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'context' => [ 'target_scope' => [ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), ], 'resource_types' => [$canonicalType], ], 'started_at' => $timestamp, 'completed_at' => $timestamp, ]); } function spec423FailingGraphClient(): GraphClientInterface { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } public function getOrganization(array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } public function request(string $method, string $path, array $options = []): GraphResponse { throw new RuntimeException('Spec423 render path must not call provider clients.'); } }; }