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 0b79db2e..6e868785 100644 --- a/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php +++ b/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php @@ -48,8 +48,8 @@ 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; + private const DEFAULT_PATH_SAMPLE_LIMIT = 2048; + private const MAX_PATH_SAMPLE_LIMIT = 2048; /** @var array> */ private array $columnCache = []; diff --git a/services/nginx/app/openapi.yaml b/services/nginx/app/openapi.yaml index 8453b6b6..f1a8d4cd 100644 --- a/services/nginx/app/openapi.yaml +++ b/services/nginx/app/openapi.yaml @@ -18441,10 +18441,10 @@ components: path_sample_limit: type: integer minimum: 1 - maximum: 200 - default: 200 + maximum: 2048 + default: 2048 nullable: true - description: Optional cap for returned path rows. Omitted and larger values are capped at 200. + description: Optional cap for returned path rows. Omitted returns every projected terminal path within the state cap; larger values are capped at 2048. SelfserveStudioPathOutcomesResponse: type: object diff --git a/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php b/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php index 48d4e3d4..01b37ee5 100644 --- a/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/SelfserveStudioGraphTest.php @@ -26,6 +26,54 @@ function selfserve_virtual_hardware_without_constructor(): selfserve_virtual_har return $reflection->newInstanceWithoutConstructor(); } +function selfserve_question_tree_simulator(int $questionCount): callable +{ + return function (array $overrides) use ($questionCount): array { + $answers = []; + foreach ($overrides as $entry) { + $answers[(int)($entry['question_id'] ?? 0)] = $entry['value'] ?? null; + } + + $questions = []; + foreach (range(1, $questionCount) as $questionId) { + $questions[] = [ + 'id' => $questionId, + 'node_id' => 'question:' . $questionId, + 'label' => 'Question ' . $questionId, + 'visible' => true, + 'answer' => $answers[$questionId] ?? null, + ]; + } + + $complete = count($answers) === $questionCount; + $allowed = $complete && !in_array(false, $answers, true); + + return [ + 'allowed' => $allowed, + 'questions' => [], + 'tasks' => $allowed ? [ + ['id' => 41, 'task' => 'Start machine', 'services' => ['MACHINE'], 'buttons' => ['start']], + ] : [], + 'allowed_services' => $allowed ? ['MACHINE'] : [], + 'debug' => [ + 'questions' => $questions, + 'tasks' => [ + [ + 'id' => 41, + 'node_id' => 'task:41', + 'label' => 'Start machine', + 'active' => $allowed, + 'services' => ['MACHINE'], + 'buttons' => ['start'], + 'order_priority' => 1, + ], + ], + 'signal_timeline' => [], + ], + ]; + }; +} + it('serializes questions, conditions, tasks, scopes, and gateways into one graph', function (): void { $service = selfserve_studio_graph_without_constructor(); @@ -1321,52 +1369,20 @@ it('truncates path outcome projection when the state cap is reached', function ( ->and($projection['warnings'][0])->toContain('truncated at 2 explored state'); }); -it('applies default caps for wide question trees and reports progress', function (): void { +it('returns more than 200 projected path cases by default', function (): void { $service = selfserve_studio_graph_without_constructor(); - $simulate = function (array $overrides): array { - $answers = []; - foreach ($overrides as $entry) { - $answers[(int)($entry['question_id'] ?? 0)] = $entry['value'] ?? null; - } + $projection = $service->projectPathOutcomesFromSimulator(selfserve_question_tree_simulator(8)); - $questions = []; - foreach (range(1, 12) as $questionId) { - $questions[] = [ - 'id' => $questionId, - 'node_id' => 'question:' . $questionId, - 'label' => 'Question ' . $questionId, - 'visible' => true, - 'answer' => $answers[$questionId] ?? null, - ]; - } + expect($projection['truncated'])->toBeFalse() + ->and($projection['summary']['state_count'])->toBe(511) + ->and($projection['summary']['terminal_path_count'])->toBe(256) + ->and($projection['summary']['path_sample_count'])->toBe(256) + ->and($projection['paths'])->toHaveCount(256); +}); - $complete = count($answers) === 12; - $allowed = $complete && !in_array(false, $answers, true); - - return [ - 'allowed' => $allowed, - 'questions' => [], - 'tasks' => $allowed ? [ - ['id' => 41, 'task' => 'Start machine', 'services' => ['MACHINE'], 'buttons' => ['start']], - ] : [], - 'allowed_services' => $allowed ? ['MACHINE'] : [], - 'debug' => [ - 'questions' => $questions, - 'tasks' => [ - [ - 'id' => 41, - 'node_id' => 'task:41', - 'label' => 'Start machine', - 'active' => $allowed, - 'services' => ['MACHINE'], - 'buttons' => ['start'], - 'order_priority' => 1, - ], - ], - 'signal_timeline' => [], - ], - ]; - }; +it('applies the default state cap for wide question trees and reports progress', function (): void { + $service = selfserve_studio_graph_without_constructor(); + $simulate = selfserve_question_tree_simulator(12); $progressEvents = []; $projection = $service->projectPathOutcomesFromSimulator($simulate, [ @@ -1385,10 +1401,10 @@ it('applies default caps for wide question trees and reports progress', function ->and($projection['summary']['question_count'])->toBe(12) ->and($projection['summary']['terminal_path_count'])->toBe(1023) ->and($projection['summary']['outcome_count'])->toBe(2) - ->and($projection['summary']['path_sample_count'])->toBe(200) + ->and($projection['summary']['path_sample_count'])->toBe(1023) ->and($projection['progress']['complete'])->toBeFalse() ->and($projection['progress']['percent'])->toBe(99) - ->and($projection['paths'])->toHaveCount(200) + ->and($projection['paths'])->toHaveCount(1023) ->and($projection['paths'][0]['answers'])->toHaveCount(12) ->and($projection['paths'][0]['result'])->toBe('Allowed') ->and($projection['warnings'][0])->toContain('truncated at 2048 explored state')