diff --git a/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php b/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php index 3621d958..474f605f 100644 --- a/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php +++ b/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php @@ -46,6 +46,11 @@ use objects\selfserve_config_versions_o; class selfserve_studio_graph { + private const DEFAULT_PATH_MAX_STATES = 2048; + private const MAX_PATH_MAX_STATES = 2048; + private const DEFAULT_PATH_SAMPLE_LIMIT = 200; + private const MAX_PATH_SAMPLE_LIMIT = 200; + /** @var array> */ private array $columnCache = []; @@ -678,7 +683,11 @@ class selfserve_studio_graph $vehicleTypeIds[] = null; } - $maxStates = $this->pathLimit($payload['max_states'] ?? null); + $maxStates = $this->pathLimit( + $payload['max_states'] ?? null, + self::DEFAULT_PATH_MAX_STATES, + self::MAX_PATH_MAX_STATES + ); $reg = trim((string)($payload['reg'] ?? $defaults['reg'] ?? 'TEST123')); if ($reg === '') { $reg = 'TEST123'; @@ -694,14 +703,18 @@ class selfserve_studio_graph $stateCount = 0; $terminalPathCount = 0; $questionIds = []; - $pathSampleLimit = $this->pathLimit($payload['path_sample_limit'] ?? null); + $pathSampleLimit = $this->pathLimit( + $payload['path_sample_limit'] ?? null, + self::DEFAULT_PATH_SAMPLE_LIMIT, + self::MAX_PATH_SAMPLE_LIMIT + ); $paths = []; $scenarioCount = max(1, count($vehicleTypeIds)); $confirmationRows = $this->loadPathConfirmationRows($departmentId, $versionId, $laneId, $vehicleTypeId, $configSource); foreach ($vehicleTypeIds as $scenarioIndex => $scenarioVehicleTypeId) { - $remainingStates = $maxStates === null ? null : $maxStates - $stateCount; - if ($remainingStates !== null && $remainingStates <= 0) { + $remainingStates = $maxStates - $stateCount; + if ($remainingStates <= 0) { $truncated = true; break; } @@ -759,7 +772,7 @@ class selfserve_studio_graph $projectionOptions = [ 'scope' => $scenarioScope, 'max_states' => $remainingStates, - 'path_sample_limit' => $pathSampleLimit === null ? null : max(0, $pathSampleLimit - count($paths)), + 'path_sample_limit' => max(0, $pathSampleLimit - count($paths)), 'progress_callback' => function (array $projection) use ( $progressCallback, &$outcomes, @@ -787,7 +800,7 @@ class selfserve_studio_graph $partialOutcomes = array_merge($outcomes, array_values((array)($projection['outcomes'] ?? []))); $partialPaths = array_merge($paths, array_values((array)($projection['paths'] ?? []))); - if ($pathSampleLimit !== null && count($partialPaths) > $pathSampleLimit) { + if (count($partialPaths) > $pathSampleLimit) { $partialPaths = array_slice($partialPaths, 0, $pathSampleLimit); } @@ -839,12 +852,6 @@ class selfserve_studio_graph }, 'confirmation_rows' => $confirmationRows, ]; - if ($remainingStates === null) { - unset($projectionOptions['max_states']); - } - if ($pathSampleLimit === null) { - unset($projectionOptions['path_sample_limit']); - } $projection = $this->projectPathOutcomesFromSimulator($simulate, $projectionOptions); foreach ((array)($projection['outcomes'] ?? []) as $outcome) { if (is_array($outcome)) { @@ -855,7 +862,7 @@ class selfserve_studio_graph if (!is_array($path)) { continue; } - if ($pathSampleLimit === null || count($paths) < $pathSampleLimit) { + if (count($paths) < $pathSampleLimit) { $paths[] = $path; } } @@ -918,9 +925,18 @@ class selfserve_studio_graph */ public function projectPathOutcomesFromSimulator(callable $simulate, array $options = []): array { - $maxStates = $this->pathLimit($options['max_states'] ?? null); + $maxStates = $this->pathLimit( + $options['max_states'] ?? null, + self::DEFAULT_PATH_MAX_STATES, + self::MAX_PATH_MAX_STATES + ); $sampleLimit = max(1, min(10, (int)($options['sample_limit'] ?? 5))); - $pathSampleLimit = $this->pathLimit($options['path_sample_limit'] ?? null); + $pathSampleLimit = $this->pathLimit( + $options['path_sample_limit'] ?? null, + self::DEFAULT_PATH_SAMPLE_LIMIT, + self::MAX_PATH_SAMPLE_LIMIT, + 0 + ); $progressCallback = is_callable($options['progress_callback'] ?? null) ? $options['progress_callback'] : null; $progressIntervalStates = max(1, (int)($options['progress_interval_states'] ?? 128)); $scope = is_array($options['scope'] ?? null) ? (array)$options['scope'] : []; @@ -938,7 +954,7 @@ class selfserve_studio_graph $truncated = false; while ($stack !== []) { - if ($maxStates !== null && $stateCount >= $maxStates) { + if ($stateCount >= $maxStates) { $truncated = true; break; } @@ -997,7 +1013,7 @@ class selfserve_studio_graph $terminalPathCount++; $chain = is_array($state['chain'] ?? null) ? (array)$state['chain'] : []; $this->addPathOutcomeGroup($groups, $simulation, $chain, $scope, $sampleLimit); - if ($pathSampleLimit === null || count($paths) < $pathSampleLimit) { + if (count($paths) < $pathSampleLimit) { $paths[] = $this->pathResultFromSimulation($simulation, $chain, $scope); } @@ -4273,14 +4289,18 @@ class selfserve_studio_graph return null; } - private function pathLimit(mixed $value): ?int + private function pathLimit(mixed $value, int $default, int $max, int $min = 1): int { if ($value === null || $value === '') { - return null; + return max($min, min($max, $default)); } $parsed = (int)$value; - return $parsed > 0 ? $parsed : null; + if ($parsed < $min) { + return max($min, min($max, $default)); + } + + return min($max, $parsed); } /** diff --git a/services/nginx/app/openapi.yaml b/services/nginx/app/openapi.yaml index 1fa7d0b2..cc892f50 100644 --- a/services/nginx/app/openapi.yaml +++ b/services/nginx/app/openapi.yaml @@ -18283,13 +18283,17 @@ components: max_states: type: integer minimum: 1 + maximum: 2048 + default: 2048 nullable: true - description: Optional debug cap. Omit for complete path projection. + description: Optional debug cap for explored states. Omitted and larger values are capped at 2048. path_sample_limit: type: integer minimum: 1 + maximum: 200 + default: 200 nullable: true - description: Optional debug cap for returned path rows. Omit to return every terminal path row. + description: Optional cap for returned path rows. Omitted and larger values are capped at 200. SelfserveStudioPathOutcomesResponse: type: object diff --git a/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php b/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php index d07611a7..49217bba 100644 --- a/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php @@ -1321,7 +1321,7 @@ it('truncates path outcome projection when the state cap is reached', function ( ->and($projection['warnings'][0])->toContain('truncated at 2 explored state'); }); -it('returns complete terminal path results for wide question trees and reports progress', function (): void { +it('applies default caps for wide question trees and reports progress', function (): void { $service = selfserve_studio_graph_without_constructor(); $simulate = function (array $overrides): array { $answers = []; @@ -1380,17 +1380,18 @@ it('returns complete terminal path results for wide question trees and reports p }, ]); - expect($projection['truncated'])->toBeFalse() - ->and($projection['summary']['state_count'])->toBe(8191) + expect($projection['truncated'])->toBeTrue() + ->and($projection['summary']['state_count'])->toBe(2048) ->and($projection['summary']['question_count'])->toBe(12) - ->and($projection['summary']['terminal_path_count'])->toBe(4096) + ->and($projection['summary']['terminal_path_count'])->toBe(1023) ->and($projection['summary']['outcome_count'])->toBe(2) - ->and($projection['summary']['path_sample_count'])->toBe(4096) + ->and($projection['summary']['path_sample_count'])->toBe(200) ->and($projection['progress']['complete'])->toBeTrue() ->and($projection['progress']['percent'])->toBe(100) - ->and($projection['paths'])->toHaveCount(4096) + ->and($projection['paths'])->toHaveCount(200) ->and($projection['paths'][0]['answers'])->toHaveCount(12) ->and($projection['paths'][0]['result'])->toBe('Allowed') + ->and($projection['warnings'][0])->toContain('truncated at 2048 explored state') ->and($progressEvents)->not->toBeEmpty() ->and($progressEvents[0]['terminal_path_count'])->toBeGreaterThan(0) ->and($progressEvents[0]['path_sample_count'])->toBeGreaterThan(0);