Add .gitattributes for binary files and extend order booking update tests. Enhance dynamic image and routing logic with program_picker support.

This commit is contained in:
Jeppe Bundgaard
2026-05-26 14:04:27 +02:00
parent f1c123a840
commit bc7c0280f2
23 changed files with 9296 additions and 3064 deletions
@@ -623,12 +623,266 @@ it('builds guided simulator debug payload with blockers and canvas annotations',
->and($debug['questions'][0]['state'])->toBe('missing')
->and($debug['questions'][0]['answer_source'])->toBe('override')
->and($debug['tasks'][0]['state'])->toBe('blocked')
->and($debug['tasks'][0]['reason'])->toBe('Task Fold mirrors blocked because gate QUESTION Are mirrors folded? expected true, actual missing.')
->and($debug['tasks'][0]['causes'][0])->toMatchArray([
'kind' => 'question',
'id' => 11,
'label' => 'Are mirrors folded?',
'expected' => true,
'actual' => null,
])
->and($debug['dynamic_image_buttons'][0])->toMatchArray([
'kind' => 'dynamic_image_button',
'label' => '1',
'state' => 'hidden',
'task_id' => 41,
])
->and(array_column($debug['decisions'], 'kind'))->toContain('question')
->and(array_column($debug['decisions'], 'kind'))->toContain('task')
->and(array_column($debug['decisions'], 'kind'))->toContain('signal')
->and(array_column($debug['decisions'], 'kind'))->toContain('dynamic_image_button')
->and(array_column($debug['recommendations'], 'title'))->toContain('Answer required questions')
->and(array_column($debug['recommendations'], 'title'))->toContain('Configure lane machine relay')
->and($debug['graph_annotations']['nodes']['question:11']['state'])->toBe('warning')
->and($debug['graph_annotations']['nodes']['lane:7']['state'])->toBe('error');
});
it('adds program picker before mapped machine buttons in simulator debug decisions', function (): void {
$service = selfserve_wash_flow_without_constructor();
$debug = $service->buildStudioDebugPayload(6, [
'lane' => [
'id' => 7,
'department' => 6,
'name' => 'Lane 7',
'relay_machine_id' => 'M-7',
'relay_machine_program_picker_id' => 'PICKER-7',
],
'machine_type' => ['id' => 1001, 'name' => 'Portal'],
'vehicle' => null,
'reg' => 'TEST123',
'customer_number' => null,
'vehicle_type_id' => 4,
'answers' => [],
'questions' => [],
'tasks' => [
['id' => 41, 'task' => 'Choose program', 'services' => ['PROGRAM_PICKER'], 'buttons' => [], 'dynamic_images_vehicle_type' => 4, 'order_priority' => 1],
['id' => 42, 'task' => 'Press first button', 'services' => ['MACHINE'], 'buttons' => [0], 'dynamic_images_vehicle_type' => 4, 'order_priority' => 2],
],
'allowed_services' => ['PROGRAM_PICKER', 'MACHINE'],
'machine_available' => true,
'all_visible_questions_answered' => true,
'allowed' => true,
'config_version_id' => 90,
'config_source' => 'draft',
'evaluation_trace' => [
'visible_question_ids' => [],
'visibility_condition_results' => [],
'condition_results' => [],
'task_gates' => [
['task_id' => 41, 'gate_type' => 'ALWAYS', 'gate_ref_id' => null, 'satisfied' => true],
['task_id' => 42, 'gate_type' => 'ALWAYS', 'gate_ref_id' => null, 'satisfied' => true],
],
],
'debug_candidates' => [
'questions' => [],
'conditions' => [],
'rules' => [],
'tasks' => [
['id' => 41, 'task' => 'Choose program', 'gate_type' => 'ALWAYS', 'gate_ref_id' => null, 'services' => ['PROGRAM_PICKER'], 'buttons' => [], 'dynamic_images_vehicle_type' => 4, 'order_priority' => 1],
['id' => 42, 'task' => 'Press first button', 'gate_type' => 'ALWAYS', 'gate_ref_id' => null, 'services' => ['MACHINE'], 'buttons' => [0], 'dynamic_images_vehicle_type' => 4, 'order_priority' => 2],
],
],
], [
'lookups' => [
'labels' => [
'departments' => ['6' => 'Roskilde'],
'lanes' => ['7' => 'Lane 7'],
'vehicle_types' => ['4' => 'Program 4'],
'machine_types' => ['1001' => 'Portal'],
'tasks' => ['41' => 'Choose program', '42' => 'Press first button'],
],
],
'gateway_workspace' => [
'gateways' => [
[
'id' => 701,
'label' => 'Roskilde Edge',
'status' => 'ONLINE',
'bindings' => [
['relay_id' => 'PICKER-7', 'label' => 'Program picker relay', 'role' => 'PROGRAM_PICKER', 'services' => ['PROGRAM_PICKER']],
['relay_id' => 'M-7', 'label' => 'Machine relay', 'role' => 'MACHINE', 'services' => ['MACHINE']],
],
],
],
],
]);
$dynamicImageDecisions = array_values(array_filter(
$debug['decisions'],
static fn(array $decision): bool => ($decision['kind'] ?? null) === 'dynamic_image_button'
));
expect(array_column($debug['dynamic_image_buttons'], 'button'))->toBe(['program_picker', 0])
->and(array_column($debug['dynamic_image_buttons'], 'label'))->toBe(['Program picker', '0'])
->and(array_column($dynamicImageDecisions, 'label'))->toBe(['Program picker', '0']);
});
it('adds exact hidden question, skipped action, signal, and button decision causes', function (): void {
$service = selfserve_wash_flow_without_constructor();
$debug = $service->buildStudioDebugPayload(6, [
'lane' => [
'id' => 7,
'department' => 6,
'name' => 'Lane 7',
'relay_machine_id' => 'M-7',
],
'machine_type' => ['id' => 1001, 'name' => 'Portal'],
'vehicle' => null,
'reg' => 'TEST123',
'customer_number' => null,
'vehicle_type_id' => 2,
'answers' => [11 => false],
'answer_sources' => [11 => 'override'],
'questions' => [],
'tasks' => [],
'allowed_services' => [],
'machine_available' => true,
'all_visible_questions_answered' => true,
'allowed' => false,
'config_version_id' => 90,
'config_source' => 'draft',
'evaluation_trace' => [
'visible_question_ids' => [11],
'visibility_condition_results' => [21 => false],
'condition_results' => [21 => false],
'visibility_expression_traces' => [
21 => [
'type' => 'condition',
'condition_id' => 21,
'result' => false,
'expression' => [
'type' => 'group',
'operator' => 'ALL',
'result' => false,
'children' => [
[
'type' => 'predicate',
'subject_type' => 'question',
'subject_id' => 11,
'operator' => 'IS_TRUE',
'actual_value' => false,
'result' => false,
'reason' => 'Predicate did not pass.',
],
],
'reason' => 'Group did not pass.',
],
],
],
'condition_expression_traces' => [
21 => [
'type' => 'condition',
'condition_id' => 21,
'result' => false,
'expression' => [
'type' => 'predicate',
'subject_type' => 'question',
'subject_id' => 11,
'operator' => 'IS_TRUE',
'actual_value' => false,
'result' => false,
'reason' => 'Predicate did not pass.',
],
],
],
'task_gates' => [
['task_id' => 41, 'gate_type' => 'QUESTION', 'gate_ref_id' => 11, 'satisfied' => false],
],
],
'debug_candidates' => [
'questions' => [
['id' => 11, 'question' => 'Are mirrors folded?', 'condition_id' => null, 'order_priority' => 1],
['id' => 12, 'question' => 'Is the lift lowered?', 'condition_id' => 21, 'order_priority' => 2],
],
'conditions' => [
['id' => 21, 'name' => 'Trailer present', 'condition_id' => null],
],
'rules' => [
['id' => 31, 'condition_id' => 21, 'type' => 'IS_TRUE', 'object_type' => 'question', 'object_id' => 11, 'name' => 'Mirror answer'],
],
'tasks' => [
['id' => 41, 'task' => 'Fold mirrors', 'gate_type' => 'QUESTION', 'gate_ref_id' => 11, 'services' => ['MACHINE'], 'buttons' => ['start'], 'order_priority' => 1],
],
'actions' => [
[
'id' => 81,
'name' => 'Open entry on start',
'event' => 'wash_start_command',
'wash_mode' => 'both',
'operation' => 'open_lane_entrance_port',
'condition_id' => 21,
'order_priority' => 1,
],
],
'visible_answers' => [11 => false],
],
], [
'lookups' => [
'labels' => [
'departments' => ['6' => 'Roskilde'],
'lanes' => ['7' => 'Lane 7'],
'vehicle_types' => ['2' => 'Forvogn'],
'machine_types' => ['1001' => 'Portal'],
'questions' => ['11' => 'Are mirrors folded?', '12' => 'Is the lift lowered?'],
'conditions' => ['21' => 'Trailer present'],
'tasks' => ['41' => 'Fold mirrors'],
],
],
'gateway_workspace' => [
'gateways' => [
[
'id' => 701,
'label' => 'Roskilde Edge',
'status' => 'ONLINE',
'bindings' => [
['relay_id' => 'M-7', 'label' => 'Machine relay', 'role' => 'MACHINE', 'services' => ['MACHINE']],
],
],
],
],
]);
$hiddenQuestion = array_values(array_filter($debug['questions'], static fn(array $question): bool => (int)$question['id'] === 12))[0] ?? [];
$taskDecision = array_values(array_filter($debug['decisions'], static fn(array $decision): bool => ($decision['kind'] ?? '') === 'task'))[0] ?? [];
$actionDecision = array_values(array_filter($debug['decisions'], static fn(array $decision): bool => ($decision['kind'] ?? '') === 'action'))[0] ?? [];
$signalDecision = array_values(array_filter($debug['decisions'], static fn(array $decision): bool => ($decision['kind'] ?? '') === 'signal' && ($decision['state'] ?? '') === 'blocked'))[0] ?? [];
$buttonDecision = array_values(array_filter($debug['decisions'], static fn(array $decision): bool => ($decision['kind'] ?? '') === 'dynamic_image_button'))[0] ?? [];
expect($hiddenQuestion['state'])->toBe('hidden')
->and($hiddenQuestion['reason'])->toBe('Question Is the lift lowered? hidden because visibility condition Trailer present expected true, actual false.')
->and($debug['conditions'][0]['causes'][0])->toMatchArray([
'kind' => 'question',
'id' => 11,
'label' => 'Are mirrors folded?',
'expected' => true,
'actual' => false,
])
->and($taskDecision['reason'])->toBe('Task Fold mirrors blocked because gate QUESTION Are mirrors folded? expected true, actual false.')
->and($actionDecision['state'])->toBe('skipped')
->and($actionDecision['causes'][0])->toMatchArray([
'kind' => 'condition',
'id' => 21,
'label' => 'Trailer present',
'expected' => true,
'actual' => false,
])
->and($signalDecision['causes'][0]['reason'])->toContain('blocked')
->and($buttonDecision['state'])->toBe('hidden')
->and($buttonDecision['causes'][0]['label'])->toBe('Fold mirrors');
});
it('projects visible question answer paths into grouped task service and signal outcomes', function (): void {
$service = selfserve_studio_graph_without_constructor();
$simulate = function (array $overrides): array {