## Summary - Add Spec 435 backend-only Exchange structured output envelope and target normalizer readiness helpers for transportRule, remoteDomain, and inboundConnector. - Add deterministic hash input/readiness handling and no-promotion guard coverage. - Add Spec 435 spec, plan, tasks, checklist, and implementation report artifacts. ## Validation - git diff --cached --check - cd apps/platform && php artisan test --filter=Spec435 --compact ## Product Surface / Deployment - Livewire v4 compliance unchanged. - Filament provider registration unchanged under apps/platform/bootstrap/providers.php. - Global search unchanged; no resources added or modified. - Destructive/high-impact actions: none. - Asset strategy: none; no filament:assets requirement for this slice. - Browser proof: N/A - no rendered UI surface changed. - Deployment impact: no env vars, migrations, queues, scheduler, storage, assets, or Dokploy runtime behavior changes. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #502
132 lines
6.0 KiB
PHP
132 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\TenantConfigurationResource;
|
|
use App\Models\TenantConfigurationResourceEvidence;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCommandContract;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellEvidenceNormalizer;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellInvocationResult;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellStructuredOutputEnvelope;
|
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
beforeEach(function (): void {
|
|
app(ResourceTypeRegistry::class)->syncDefaults();
|
|
});
|
|
|
|
it('Spec435 readiness evaluation remains in-memory and creates no resources evidence or operation runs', function (): void {
|
|
$beforeRuns = OperationRun::query()->count();
|
|
|
|
$readiness = app(ExchangePowerShellEvidenceNormalizer::class)->evaluate(
|
|
spec435FeatureEnvelope('transportRule', [[
|
|
'Guid' => 'rule-guid-1',
|
|
'DisplayName' => 'Rule 1',
|
|
'Enabled' => true,
|
|
]]),
|
|
);
|
|
|
|
expect($readiness->ready)->toBeTrue()
|
|
->and(OperationRun::query()->count())->toBe($beforeRuns)
|
|
->and(TenantConfigurationResource::query()->count())->toBe(0)
|
|
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('Spec435 empty collections create no durable collection proof resource or evidence row', function (): void {
|
|
$readiness = app(ExchangePowerShellEvidenceNormalizer::class)->evaluate(
|
|
spec435FeatureEnvelope('transportRule', [], 'empty_collection'),
|
|
);
|
|
|
|
expect($readiness->ready)->toBeTrue()
|
|
->and($readiness->emptyCollection)->toBeTrue()
|
|
->and($readiness->normalizedPreviews)->toBe([])
|
|
->and($readiness->hashPreviews)->toBe([])
|
|
->and(TenantConfigurationResource::query()->count())->toBe(0)
|
|
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('Spec435 readiness code stays separated from capture upsert and evidence append paths', function (): void {
|
|
$runtimeSource = collect(spec435RuntimeFiles())
|
|
->map(fn (string $file): string => file_get_contents($file) ?: '')
|
|
->implode("\n");
|
|
|
|
expect($runtimeSource)
|
|
->not->toContain('ExchangePowerShellEvidenceCaptureAdapter::capture')
|
|
->not->toContain('CoverageResourceUpserter')
|
|
->not->toContain('CoverageEvidenceWriter')
|
|
->not->toContain('TenantConfigurationResourceEvidence::')
|
|
->not->toContain('content_backed')
|
|
->not->toContain('comparable')
|
|
->not->toContain('renderable')
|
|
->not->toContain('certified')
|
|
->not->toContain('restore_ready')
|
|
->not->toContain('customer_ready');
|
|
});
|
|
|
|
it('Spec435 introduces no product surface triggers migrations tenant-id ownership or mini-platform', function (): void {
|
|
$runtimeSource = collect(spec435RuntimeFiles())
|
|
->map(fn (string $file): string => file_get_contents($file) ?: '')
|
|
->implode("\n");
|
|
|
|
expect(preg_match('/\btenant_id\b/', $runtimeSource))->toBe(0)
|
|
->and(Schema::hasColumn('tenant_configuration_resources', 'tenant_id'))->toBeFalse()
|
|
->and(Schema::hasColumn('tenant_configuration_resource_evidence', 'tenant_id'))->toBeFalse()
|
|
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([])
|
|
->and(glob(app_path('Livewire/**/*ExchangePowerShell*')) ?: [])->toBe([])
|
|
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
|
|
->and(glob(resource_path('views/**/*ExchangePowerShell*')) ?: [])->toBe([])
|
|
->and(glob(database_path('migrations/*exchange*evidence*')) ?: [])->toBe([])
|
|
->and(glob(database_path('migrations/*tenant*configuration*exchange*')) ?: [])->toBe([])
|
|
->and(File::exists(app_path('Jobs/TenantConfiguration/CaptureExchangePowerShellEvidenceJob.php')))->toBeFalse()
|
|
->and(File::exists(app_path('Jobs/TenantConfiguration/NormalizeExchangePowerShellEvidenceJob.php')))->toBeFalse()
|
|
->and(File::exists(app_path('Console/Commands/CaptureExchangePowerShellEvidence.php')))->toBeFalse();
|
|
});
|
|
|
|
function spec435FeatureEnvelope(
|
|
string $canonicalType,
|
|
array $items,
|
|
string $outputState = 'structured_collection',
|
|
): ExchangePowerShellStructuredOutputEnvelope {
|
|
return ExchangePowerShellStructuredOutputEnvelope::fromInvocationResult(
|
|
resourceType: $canonicalType,
|
|
contract: spec435FeatureVerifiedContract($canonicalType),
|
|
runnerMode: 'fake',
|
|
result: ExchangePowerShellInvocationResult::succeeded($items, context: [
|
|
'output_state' => $outputState,
|
|
'item_count' => count($items),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function spec435FeatureVerifiedContract(string $canonicalType): ExchangePowerShellCommandContract
|
|
{
|
|
$contracts = new ExchangePowerShellCommandContracts;
|
|
$contract = $contracts->contractForCanonicalType($canonicalType);
|
|
|
|
if (! is_array($contract)) {
|
|
throw new RuntimeException('Missing Spec435 feature command contract.');
|
|
}
|
|
|
|
return ExchangePowerShellCommandContract::fromVerifiedArray($contract, $contracts, []);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
function spec435RuntimeFiles(): array
|
|
{
|
|
return [
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellStructuredOutputEnvelope.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellEvidenceNormalizer.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellTargetEvidenceNormalizer.php'),
|
|
app_path('Services/TenantConfiguration/ExchangeTransportRuleEvidenceNormalizer.php'),
|
|
app_path('Services/TenantConfiguration/ExchangeRemoteDomainEvidenceNormalizer.php'),
|
|
app_path('Services/TenantConfiguration/ExchangeInboundConnectorEvidenceNormalizer.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellNormalizerReadiness.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellHashInputBuilder.php'),
|
|
];
|
|
}
|