Add department_selfserve_path_confirmations table and enhance PingApiTest

Introduce a new database table `department_selfserve_path_confirmations` to store path confirmations related to department configurations. Update `PingApiTest` to verify additional keys, ensuring `backend_version` and `api_commit_sha` are checked in the response.
This commit is contained in:
Jeppe Bundgaard
2026-05-27 17:35:16 +02:00
parent d52ceb8513
commit b7aeb11801
15 changed files with 2104 additions and 46 deletions
@@ -370,6 +370,168 @@ it('keeps runtime on published v2 configs and leaves draft JSON as the studio ed
expect($washFlowSource)->toContain("\$configSource = \$publishedConfigPayload === null ? 'legacy' : 'published';");
expect($studioGraphSource)->toContain('$draftObject->config_json->set($config);');
expect($studioGraphSource)->toContain('Standalone rule operations are not supported in self-serve rules v2.');
expect($studioGraphSource)->toContain('upsert_path');
});
it('upserts path editor answers into generated condition and task config rows', function (): void {
$service = selfserve_studio_graph_without_constructor();
$method = new ReflectionMethod(selfserve_studio_graph::class, 'applyConfigOperation');
$method->setAccessible(true);
$config = [
'schema_version' => 2,
'questions' => [
['id' => 11, 'question' => 'Machine wash is allowed', 'order_priority' => 1],
['id' => 12, 'question' => 'Trailer present', 'order_priority' => 2],
],
'conditions' => [],
'rules' => [],
'tasks' => [],
'actions' => [],
'v2_meta' => ['next_ids' => ['condition' => 100, 'task' => 200]],
];
$method->invokeArgs($service, [6, &$config, [
'action' => 'upsert_path',
'entity' => 'path',
'data' => [
'path_key' => 'allowed_front',
'scope' => ['lane_id' => 7, 'vehicle_type_id' => 2, 'machine_type_id' => 1001],
'answers' => [
['question_id' => 11, 'value' => true],
['question_id' => 12, 'value' => false],
],
'result' => [
'machine_allowed' => true,
'task' => 'Set program',
'services' => ['MACHINE', 'PROGRAM_PICKER'],
'buttons' => ['program_picker', 'reset', 2, 'start'],
'dynamic_images_vehicle_type' => 3,
'tasks' => [
[
'task' => 'Set program',
'services' => ['MACHINE', 'PROGRAM_PICKER'],
'buttons' => ['program_picker'],
'dynamic_images_vehicle_type' => 3,
],
[
'task' => 'Press reset',
'services' => ['MACHINE'],
'buttons' => ['reset'],
],
[
'task' => 'Press machine button 2',
'services' => ['MACHINE'],
'buttons' => [2],
],
[
'task' => 'Press start',
'services' => ['MACHINE'],
'buttons' => ['start'],
],
],
],
],
]]);
$condition = $config['conditions'][0] ?? [];
$tasks = array_values($config['tasks'] ?? []);
$pathMeta = $config['v2_meta']['path_editor']['paths']['allowed_front'] ?? [];
$validation = (new class extends selfserve_config_versioning {
public function __construct()
{
}
})->validateConfig($config);
expect($condition['generated_by'])->toBe('path_editor')
->and($condition['path_key'])->toBe('allowed_front')
->and($condition['lane'])->toBe(7)
->and($condition['product'])->toBe(2)
->and($condition['machine_type_id'])->toBe(1001)
->and($condition['expression']['children'])->toHaveCount(2)
->and($condition['expression']['children'][0]['operator'])->toBe('IS_TRUE')
->and($condition['expression']['children'][1]['operator'])->toBe('IS_FALSE')
->and($tasks)->toHaveCount(4)
->and(array_column($tasks, 'task'))->toBe(['Set program', 'Press reset', 'Press machine button 2', 'Press start'])
->and(array_column($tasks, 'order_priority'))->toBe([10, 20, 30, 40])
->and(array_column($tasks, 'gate_ref_id'))->toBe([(int)$condition['id'], (int)$condition['id'], (int)$condition['id'], (int)$condition['id']])
->and($tasks[0]['generated_by'])->toBe('path_editor')
->and($tasks[0]['gate_type'])->toBe('CONDITION')
->and($tasks[0]['services'])->toBe(['MACHINE', 'PROGRAM_PICKER'])
->and($tasks[0]['buttons'])->toBe(['program_picker'])
->and($tasks[0]['dynamic_images_vehicle_type'])->toBe(3)
->and($tasks[1]['buttons'])->toBe(['reset'])
->and($tasks[2]['buttons'])->toBe([2])
->and($tasks[3]['buttons'])->toBe(['start'])
->and($pathMeta['condition_id'])->toBe((int)$condition['id'])
->and($pathMeta['task_id'])->toBe((int)$tasks[0]['id'])
->and($pathMeta['task_ids'])->toBe(array_map(static fn(array $task): int => (int)$task['id'], $tasks))
->and($pathMeta['result']['buttons'])->toBe(['program_picker', 'reset', 2, 'start'])
->and($pathMeta['path_signature'])->not->toBe('')
->and($validation['valid'])->toBeTrue();
$method->invokeArgs($service, [6, &$config, [
'action' => 'upsert_path',
'entity' => 'path',
'data' => [
'path_key' => 'allowed_front',
'scope' => ['lane_id' => 7, 'vehicle_type_id' => 2, 'machine_type_id' => 1001],
'answers' => [
['question_id' => 11, 'value' => true],
['question_id' => 12, 'value' => false],
],
'result' => [
'machine_allowed' => true,
'task' => 'Set program',
'services' => ['MACHINE', 'PROGRAM_PICKER'],
'buttons' => ['program_picker', 'start'],
'dynamic_images_vehicle_type' => 3,
'tasks' => [
['task' => 'Set program', 'services' => ['MACHINE', 'PROGRAM_PICKER'], 'buttons' => ['program_picker'], 'dynamic_images_vehicle_type' => 3],
['task' => 'Press start', 'services' => ['MACHINE'], 'buttons' => ['start']],
],
],
],
]]);
$updatedTasks = array_values(array_filter(
$config['tasks'] ?? [],
static fn(array $task): bool => ($task['path_key'] ?? '') === 'allowed_front'
));
$updatedPathMeta = $config['v2_meta']['path_editor']['paths']['allowed_front'] ?? [];
expect($updatedTasks)->toHaveCount(2)
->and(array_column($updatedTasks, 'id'))->toBe([(int)$tasks[0]['id'], (int)$tasks[1]['id']])
->and(array_column($updatedTasks, 'task'))->toBe(['Set program', 'Press start'])
->and($updatedPathMeta['task_ids'])->toBe([(int)$tasks[0]['id'], (int)$tasks[1]['id']]);
$method->invokeArgs($service, [6, &$config, [
'action' => 'upsert_path',
'entity' => 'path',
'data' => [
'path_key' => 'legacy_front',
'scope' => ['lane_id' => 7, 'vehicle_type_id' => 2, 'machine_type_id' => 1001],
'answers' => [
['question_id' => 11, 'value' => false],
],
'result' => [
'machine_allowed' => true,
'task' => 'Legacy start',
'services' => ['MACHINE'],
'buttons' => ['start'],
],
],
]]);
$legacyPathMeta = $config['v2_meta']['path_editor']['paths']['legacy_front'] ?? [];
$legacyTask = array_values(array_filter(
$config['tasks'] ?? [],
static fn(array $task): bool => ($task['path_key'] ?? '') === 'legacy_front'
))[0] ?? [];
expect($legacyTask['task'])->toBe('Legacy start')
->and($legacyTask['buttons'])->toBe(['start'])
->and($legacyPathMeta['task_id'])->toBe((int)$legacyTask['id'])
->and($legacyPathMeta['task_ids'])->toBe([(int)$legacyTask['id']]);
});
it('surfaces task attachments in studio graph, simulator, and flow responses', function (): void {
@@ -1016,10 +1178,93 @@ it('projects visible question answer paths into grouped task service and signal
->and($allowedPath['answers'])->toHaveCount(2)
->and($allowedPath['answers'][0]['question'])->toBe('Are mirrors folded?')
->and($allowedPath['services'])->toBe(['MACHINE'])
->and($allowedPath['path_signature'])->not->toBe('')
->and($allowedPath['result_signature'])->not->toBe('')
->and($allowedPath['confirmation_status'])->toBe('unconfirmed')
->and($allowedPath['node_ids'])->toContain('question:11')
->and($allowedPath['node_ids'])->toContain('task:41');
});
it('marks projected path confirmations confirmed or stale by stable signatures', function (): void {
$service = selfserve_studio_graph_without_constructor();
$simulate = static function (string $button): callable {
return static function (array $overrides) use ($button): array {
$answers = [];
foreach ($overrides as $entry) {
$answers[(int)($entry['question_id'] ?? 0)] = $entry['value'] ?? null;
}
$allowed = ($answers[11] ?? null) === true;
return [
'allowed' => $allowed,
'questions' => [
['id' => 11, 'question' => 'Machine wash is allowed', 'answer' => $answers[11] ?? null],
],
'tasks' => $allowed ? [
['id' => 41, 'task' => 'Start machine', 'services' => ['MACHINE'], 'buttons' => [$button]],
] : [],
'allowed_services' => $allowed ? ['MACHINE'] : [],
'debug' => [
'questions' => [
['id' => 11, 'node_id' => 'question:11', 'label' => 'Machine wash is allowed', 'visible' => true, 'answer' => $answers[11] ?? null],
],
'tasks' => [
['id' => 41, 'node_id' => 'task:41', 'label' => 'Start machine', 'active' => $allowed, 'services' => ['MACHINE'], 'buttons' => [$button], 'order_priority' => 1],
],
'signal_timeline' => [],
],
];
};
};
$scope = [
'department_id' => 6,
'lane_id' => 7,
'vehicle_type_id' => 2,
'config_source' => 'draft',
'config_version_id' => 90,
'hardware_mode' => 'studio',
];
$initial = $service->projectPathOutcomesFromSimulator($simulate('start'), ['scope' => $scope]);
$allowedPath = array_values(array_filter(
$initial['paths'],
static fn(array $path): bool => ($path['allowed'] ?? false) === true
))[0] ?? [];
$rows = [[
'path_signature' => $allowedPath['path_signature'],
'result_signature' => $allowedPath['result_signature'],
'answers' => $allowedPath['answers'],
'result' => ['allowed' => true],
'scope' => $scope,
'confirmed_at' => '2026-05-27 10:00:00',
'confirmed_by' => 9,
]];
$confirmed = $service->projectPathOutcomesFromSimulator($simulate('start'), [
'scope' => $scope,
'confirmation_rows' => $rows,
]);
$changed = $service->projectPathOutcomesFromSimulator($simulate('reset'), [
'scope' => $scope,
'confirmation_rows' => $rows,
]);
$confirmedAllowed = array_values(array_filter(
$confirmed['paths'],
static fn(array $path): bool => ($path['allowed'] ?? false) === true
))[0] ?? [];
$staleAllowed = array_values(array_filter(
$changed['paths'],
static fn(array $path): bool => ($path['allowed'] ?? false) === true
))[0] ?? [];
expect($confirmedAllowed['confirmation_status'])->toBe('confirmed')
->and($confirmed['summary']['confirmations']['confirmed'])->toBe(1)
->and($staleAllowed['confirmation_status'])->toBe('stale')
->and($staleAllowed['stale_reason'])->toBe('Result changed since confirmation.')
->and($changed['summary']['confirmations']['stale'])->toBe(1);
});
it('truncates path outcome projection when the state cap is reached', function (): void {
$service = selfserve_studio_graph_without_constructor();
$simulate = function (array $overrides): array {