TenantAtlas/tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php
2026-03-28 15:57:45 +01:00

202 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
use Symfony\Component\Yaml\Yaml;
use Tests\Support\OpsUx\SourceFileScanner;
it('keeps covered derived-state consumers on declared access paths without ad hoc caches', function (): void {
$root = SourceFileScanner::projectRoot();
$contractPath = $root.'/specs/167-derived-state-memoization/contracts/request-scoped-derived-state.logical.openapi.yaml';
/** @var array<string, mixed> $contract */
$contract = Yaml::parseFile($contractPath);
$declarations = $contract['x-derived-state-consumers'] ?? [];
expect($declarations)->toBeArray()->not->toBeEmpty();
$allowedFamilies = [
'artifact_truth',
'operation_ux_guidance',
'operation_ux_explanation',
'related_navigation_primary',
'related_navigation_detail',
'related_navigation_header',
];
$allowedAccessPatterns = ['row_safe', 'page_safe', 'direct_once'];
$allowedFreshnessPolicies = ['request_stable', 'invalidate_after_mutation', 'no_reuse'];
$cachePattern = '/static\s+array\s+\$[A-Za-z0-9_]*cache\b/i';
$violations = [];
foreach ($declarations as $index => $declaration) {
if (! is_array($declaration)) {
$violations[] = [
'surface' => 'contract',
'message' => sprintf('Declaration %d must be an object.', $index),
];
continue;
}
$surface = trim((string) ($declaration['surface'] ?? ''));
$family = trim((string) ($declaration['family'] ?? ''));
$variant = trim((string) ($declaration['variant'] ?? ''));
$accessPattern = trim((string) ($declaration['accessPattern'] ?? ''));
$freshnessPolicy = trim((string) ($declaration['freshnessPolicy'] ?? ''));
$scopeInputs = $declaration['scopeInputs'] ?? null;
$guardScope = $declaration['guardScope'] ?? null;
$requiredMarkers = array_values(array_filter(
$declaration['requiredMarkers'] ?? [],
static fn (mixed $marker): bool => is_string($marker) && trim($marker) !== '',
));
$maxOccurrences = $declaration['maxOccurrences'] ?? [];
if ($surface === '' || $variant === '') {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => 'Each declaration must provide non-empty surface and variant values.',
];
}
if (! in_array($family, $allowedFamilies, true)) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => sprintf('Unsupported family "%s".', $family),
];
}
if (! in_array($accessPattern, $allowedAccessPatterns, true)) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => sprintf('Unsupported accessPattern "%s".', $accessPattern),
];
}
if (! in_array($freshnessPolicy, $allowedFreshnessPolicies, true)) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => sprintf('Unsupported freshnessPolicy "%s".', $freshnessPolicy),
];
}
if (! is_array($scopeInputs) || $scopeInputs === []) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => 'Each declaration must include at least one scope input.',
];
}
if (! is_array($guardScope) || $guardScope === []) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => 'Each declaration must include at least one guardScope path.',
];
continue;
}
foreach ($guardScope as $relativePath) {
$relativePath = trim((string) $relativePath);
$absolutePath = $root.'/'.$relativePath;
if ($relativePath === '' || ! is_file($absolutePath)) {
$violations[] = [
'surface' => $surface !== '' ? $surface : 'contract',
'message' => sprintf('Missing guardScope file "%s".', $relativePath),
];
continue;
}
$source = SourceFileScanner::read($absolutePath);
foreach ($requiredMarkers as $marker) {
if (str_contains($source, $marker)) {
continue;
}
$violations[] = [
'surface' => $surface,
'file' => SourceFileScanner::relativePath($absolutePath),
'message' => sprintf('Missing required marker "%s".', $marker),
];
}
if (preg_match($cachePattern, $source, $match, PREG_OFFSET_CAPTURE) === 1) {
$offset = $match[0][1];
$line = substr_count(substr($source, 0, is_int($offset) ? $offset : 0), "\n") + 1;
$violations[] = [
'surface' => $surface,
'file' => SourceFileScanner::relativePath($absolutePath),
'line' => $line,
'message' => 'Ad hoc local cache detected in guarded surface.',
'snippet' => SourceFileScanner::snippet($source, $line),
];
}
if (! is_array($maxOccurrences)) {
continue;
}
foreach ($maxOccurrences as $occurrenceRule) {
if (! is_array($occurrenceRule)) {
continue;
}
$needle = trim((string) ($occurrenceRule['needle'] ?? ''));
$max = (int) ($occurrenceRule['max'] ?? -1);
if ($needle === '' || $max < 0) {
continue;
}
$actual = substr_count($source, $needle);
if ($actual <= $max) {
continue;
}
$offset = strpos($source, $needle);
$line = $offset === false ? 1 : substr_count(substr($source, 0, $offset), "\n") + 1;
$violations[] = [
'surface' => $surface,
'file' => SourceFileScanner::relativePath($absolutePath),
'line' => $line,
'message' => sprintf('Found %d occurrences of "%s"; expected at most %d.', $actual, $needle, $max),
'snippet' => SourceFileScanner::snippet($source, $line),
];
}
}
}
if ($violations !== []) {
$messages = array_map(static function (array $violation): string {
$location = $violation['surface'] ?? 'contract';
if (isset($violation['file'])) {
$location .= ' @ '.$violation['file'];
}
if (isset($violation['line'])) {
$location .= ':'.$violation['line'];
}
$message = $location."\n".$violation['message'];
if (isset($violation['snippet'])) {
$message .= "\n".$violation['snippet'];
}
return $message;
}, $violations);
$this->fail(
"Derived-state consumer guard violations found:\n\n".implode("\n\n", $messages)
);
}
expect($violations)->toBe([]);
});