newInstanceWithoutConstructor(); } it('serializes questions, conditions, tasks, scopes, and gateways into one graph', function (): void { $service = selfserve_studio_graph_without_constructor(); $graph = $service->buildGraphFromConfig([ 'questions' => [ ['id' => 1, 'question' => 'Are mirrors folded?', 'condition_id' => 10, 'lane' => 7, 'product' => 3, 'department' => 2, 'order_priority' => 1], ], 'conditions' => [ ['id' => 10, 'name' => 'Trailer present', 'condition_id' => null, 'lane' => 7, 'product' => 3, 'department' => 2], ], 'rules' => [ ['id' => 20, 'condition_id' => 10, 'type' => 'IS_TRUE', 'object_type' => 'question', 'object_id' => 1, 'name' => 'Mirror answer'], ], 'tasks' => [ ['id' => 30, 'task' => 'Fold mirrors', 'gate_type' => 'QUESTION', 'gate_ref_id' => 1, 'lane' => 7, 'product' => 3, 'department' => 2, 'order_priority' => 1], ], ], [ 'lookups' => [ 'departments' => [['id' => 2, 'label' => 'Roskilde']], 'lanes' => [['id' => 7, 'label' => 'Lane 7']], 'products' => [['id' => 3, 'label' => 'Forvogn']], 'machine_types' => [], 'vehicle_types' => [['id' => 3, 'product' => 3, 'label' => 'Forvogn', 'source' => 'products']], 'labels' => [ 'departments' => ['2' => 'Roskilde'], 'lanes' => ['7' => 'Lane 7'], 'products' => ['3' => 'Forvogn'], 'vehicle_types' => ['3' => 'Forvogn'], 'questions' => ['1' => 'Are mirrors folded?'], 'conditions' => ['10' => 'Trailer present'], 'tasks' => ['30' => 'Fold mirrors'], ], ], 'gateway_workspace' => [ 'gateways' => [ [ 'id' => 50, 'label' => 'Gateway A', 'status' => 'ONLINE', 'bindings' => [ ['relay_id' => 'relay-1', 'label' => 'Machine relay'], ], ], ], 'relays' => [ ['relay_id' => 'relay-1', 'name' => 'Machine relay'], ], 'lanes' => [ [ 'id' => 7, 'relay_slots' => [ ['relay_id' => 'relay-1', 'slot' => 'MACHINE'], ], ], ], ], ]); $nodeIds = array_column($graph['nodes'], 'id'); $edgeIds = array_column($graph['edges'], 'id'); expect($nodeIds)->toContain('question:1'); expect($nodeIds)->toContain('condition:10'); expect($nodeIds)->toContain('rule:20'); expect($nodeIds)->toContain('task:30'); expect($nodeIds)->toContain('vehicle_type:3'); expect($nodeIds)->toContain('gateway:50'); expect($nodeIds)->toContain('relay:relay-1'); expect($edgeIds)->toContain('question-gate:10:1'); expect($edgeIds)->toContain('task-gate:question:1:30'); expect($edgeIds)->toContain('scope:vehicle_type:3:question:1'); expect($edgeIds)->toContain('scope:vehicle_type:3:condition:10'); expect($edgeIds)->toContain('scope:vehicle_type:3:task:30'); expect($edgeIds)->toContain('gateway-binding:50:relay-1:0'); expect($edgeIds)->toContain('relay-lane:relay-1:7:MACHINE'); }); it('derives studio vehicle type lookup rows from selectable wash products', function (): void { $service = selfserve_studio_graph_without_constructor(); $method = new ReflectionMethod(selfserve_studio_graph::class, 'vehicleTypeRowsFromProducts'); $vehicleTypes = $method->invoke($service, [ ['id' => 3, 'name' => 'Forvogn', 'description' => 'Front vehicle', 'is_wash' => 1, 'subscription_allowed' => 1, 'order_priority' => 10], ['id' => 4, 'name' => 'Trækker', 'description' => 'Tractor unit', 'is_wash' => '1', 'subscription_allowed' => '1', 'order_priority' => 20], ['id' => 5, 'name' => 'Addon', 'description' => '', 'is_wash' => 0, 'subscription_allowed' => 1, 'order_priority' => 30], ['id' => 6, 'name' => 'Internal wash', 'description' => '', 'is_wash' => 1, 'subscription_allowed' => 0, 'order_priority' => 40], ]); expect(array_column($vehicleTypes, 'id'))->toBe([3, 4]); expect(array_column($vehicleTypes, 'label'))->toBe(['Forvogn', 'Trækker']); expect($vehicleTypes[0]['product'])->toBe(3); expect($vehicleTypes[0]['product_id'])->toBe(3); expect($vehicleTypes[0]['source'])->toBe('products'); }); it('applies saved layout without changing graph semantics', function (): void { $service = selfserve_studio_graph_without_constructor(); $graph = $service->buildGraphFromConfig([ 'questions' => [ ['id' => 1, 'question' => 'Question', 'order_priority' => 1], ], 'conditions' => [], 'rules' => [], 'tasks' => [], ], [ 'lookups' => [ 'labels' => [], ], ], [ 'nodes' => [ 'question:1' => ['x' => 123, 'y' => 456], ], ]); $questionNode = array_values(array_filter( $graph['nodes'], static fn(array $node): bool => ($node['id'] ?? null) === 'question:1' ))[0] ?? null; expect($questionNode)->not->toBeNull(); expect($questionNode['position'])->toBe(['x' => 123.0, 'y' => 456.0]); }); it('keeps layout loading compatible with native PDO named placeholders', function (): void { $method = new ReflectionMethod(selfserve_studio_graph::class, 'loadLayout'); $source = implode('', array_slice( file((string)$method->getFileName()) ?: [], $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1 )); expect($source)->not->toContain('user_id = :user_id OR user_id IS NULL'); expect($source)->not->toContain('user_id = :user_id THEN 0 ELSE 1'); expect($source)->toContain(':user_id_filter'); expect($source)->toContain(':user_id_sort'); });