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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user