Los catalogos de los campos select y multi_select pedian la lista mandando solo
lo recolectado en el formulario, nunca el metadata de la conversacion: la finca
elegida no viajaba, {finca_id} quedaba sin resolver en la URL y el ERP lo leia
como 0 —todas las fincas—. Elegir REPOSO y ver lotes de otras fincas, tal cual
lo reporto el usuario.
Ahora el metadata acompana lo recolectado en ambos puntos. Los fixtures delatan
la fuga —sin finca_id valido cuelan un lote ajeno— y dos casos nuevos verifican
que a los catalogos de ciclos y labores solo lleguen lotes de la finca elegida.
De paso el select simple gana resolverPorCampos en su clave, que el multi ya
tenia: un catalogo por campo puede depender de lo elegido antes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
261 lines
13 KiB
PHP
261 lines
13 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Tests unitarios de los flujos de carga, ejecutando NormalBot de verdad.
|
|
*
|
|
* A diferencia de test_navegacion.php (que espeja la lógica), acá corre
|
|
* process()/processInteractive() reales: el curl pega contra fixtures_api.php
|
|
* y los POST capturados se comparan contra el contrato del ERP.
|
|
*
|
|
* Uso: php setup/tests/run.sh (levanta el servidor de fixtures y corre esto)
|
|
*/
|
|
|
|
require __DIR__ . '/bootstrap.php';
|
|
|
|
$co = empresa();
|
|
|
|
// ════ 1. Finca restringida: una sola → entra directo ═════════════════════════
|
|
echo "\nFinca — alcance por número\n";
|
|
|
|
$ctx = contexto('57300RESTRINGIDO');
|
|
$r = NormalBot::process($co, $ctx, 'hola');
|
|
check('con una sola finca no pregunta: cae al menú',
|
|
str_contains(textoDe($r), '¿Qué deseas hacer?'));
|
|
$meta = ConversationContext::getMetadata(ConversationContext::getOrCreate(1, '57300RESTRINGIDO')['id']);
|
|
check('la finca quedó elegida sola', $meta['finca']['finca_id'] ?? null, '4');
|
|
check('con su nombre para el footer', $meta['finca']['finca_label'] ?? null, 'REPOSO');
|
|
$p = json_decode($r['payload'], true);
|
|
check('y el footer la muestra', $p['interactive']['footer']['text'] ?? '', '📍 REPOSO');
|
|
|
|
$ctx = contexto('57300LIBRE');
|
|
$r = NormalBot::process($co, $ctx, 'hola');
|
|
check('sin restricción sí pregunta, con "Todas" primero',
|
|
(string)array_key_first(filasDe($r)), '0');
|
|
capturas();
|
|
|
|
// ════ 2. Ausentismo completo ═════════════════════════════════════════════════
|
|
echo "\nAusentismo — de la primera pregunta al POST\n";
|
|
|
|
$ctx = contexto('57300AUSEN');
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
|
check('arranca pidiendo el trabajador', str_contains(textoDe($r), 'nombre o documento'));
|
|
|
|
$r = NormalBot::process($co, $ctx, 'julio');
|
|
check('varios homónimos: ofrece elegir', str_contains(textoDe($r), 'coincidencias'));
|
|
check('con los tres del catálogo', count(filasDe($r)), 3);
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '412');
|
|
$filas = filasDe($r);
|
|
check('sigue el motivo, paginado: 9 + "Ver más"', count($filas), 10);
|
|
check('la última fila es "Ver más"', isset($filas['__mas']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '__mas');
|
|
$filas = filasDe($r);
|
|
check('la página 2 trae las 8 restantes', count($filas), 8);
|
|
check('y ya sin "Ver más"', !isset($filas['__mas']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '112');
|
|
check('sigue la fecha inicial', str_contains(textoDe($r), 'Desde qué fecha'));
|
|
|
|
$hoy = date('Y-m-d'); $ayer = date('Y-m-d', strtotime('-1 day'));
|
|
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
|
check('sigue la fecha final', str_contains(textoDe($r), 'Hasta qué fecha'));
|
|
|
|
WhatsAppSender::$enviados = [];
|
|
$r = NormalBot::processInteractive($co, $ctx, $ayer);
|
|
check('rechaza el rango invertido',
|
|
str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', 'no puede ser anterior'));
|
|
check('y vuelve a mostrar el selector de fecha', str_contains(textoDe($r), 'Hasta qué fecha'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
|
check('resumen con confirmación', isset(filasDe($r)['__cap_confirm']));
|
|
|
|
capturas(); // limpia GETs de catálogos
|
|
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
|
$post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'ausentismos_up'))[0] ?? null;
|
|
check('el POST llegó al ERP', $post !== null);
|
|
check('con el contrato exacto del procesador',
|
|
[$post['body']['empleado_id'] ?? null, $post['body']['novedad_id'] ?? null,
|
|
$post['body']['fecha_inicial'] ?? null, $post['body']['fecha_final'] ?? null],
|
|
['412', '112', $hoy, $hoy]);
|
|
check('y la trazabilidad', $post['body']['telefono'] ?? null, '57300AUSEN');
|
|
|
|
// ════ 3. Ausentismo según el perfil del número ═══════════════════════════════
|
|
echo "\nPerfil — trabajador vinculado y supervisor\n";
|
|
|
|
fakePhone('57300TRABAJADOR', 412, 0);
|
|
fakePhone('57300JEFE', 412, 1);
|
|
|
|
$ctx = contexto('57300TRABAJADOR');
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
|
check('el trabajador vinculado no se pregunta: arranca por el motivo',
|
|
str_contains(textoDe($r), 'motivo del ausentismo'));
|
|
|
|
$ctx = contexto('57300JEFE');
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
|
check('el supervisor elige aunque tenga tercero',
|
|
str_contains(textoDe($r), 'nombre o documento'));
|
|
capturas();
|
|
|
|
// ════ 4. Ciclos: apertura con selección múltiple ═════════════════════════════
|
|
echo "\nCiclos — apertura de varios lotes\n";
|
|
|
|
$ctx = contexto('57300CICLOS');
|
|
$id = ConversationContext::getOrCreate(1, '57300CICLOS')['id'];
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_ciclo');
|
|
check('pregunta el tipo de ciclo', str_contains(textoDe($r), 'Qué ciclo'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, 'cosecha');
|
|
check('después la acción', isset(filasDe($r)['apertura']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, 'apertura');
|
|
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
|
$filas = filasDe($r);
|
|
check('lista de lotes con "Otro lote" y "Listo"',
|
|
isset($filas['__todos']) && isset($filas['__listo']));
|
|
check('solo lotes de la finca elegida, ninguno ajeno',
|
|
!isset($filas['99']) && !str_contains(implode(' ', $filas), 'ROSA BLANCA'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '1');
|
|
check('marcar redibuja con el check', str_contains(filasDe($r)['1'] ?? '', '✅'));
|
|
$r = NormalBot::processInteractive($co, $ctx, '2');
|
|
check('el contador acumula', str_contains(filasDe($r)['__listo'] ?? '', '(2)'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '__todos');
|
|
check('"Otro lote" pasa al catálogo completo, paginado', isset(filasDe($r)['__mas']));
|
|
check('sin perder lo marcado', str_contains(filasDe($r)['1'] ?? '', '✅'));
|
|
$r = NormalBot::processInteractive($co, $ctx, '__mas');
|
|
check('y la página 2 alcanza el lote 12', isset(filasDe($r)['12']));
|
|
$r = NormalBot::processInteractive($co, $ctx, '__mas'); // vuelve: no hay página 3
|
|
$r = NormalBot::processInteractive($co, $ctx, '__mas');
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '__listo');
|
|
check('resumen final', isset(filasDe($r)['__cap_confirm']));
|
|
|
|
capturas();
|
|
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
|
$post = array_values(array_filter(capturas(), fn($c) => str_contains($c['peticion'] ?? '', 'ciclos_')))[0] ?? null;
|
|
check('la URL resolvió el tipo elegido', $post['peticion'] ?? null, 'ciclos_cosecha_up');
|
|
check('lote_ids como arreglo', $post['body']['lote_ids'] ?? null, ['1', '2']);
|
|
check('con la acción y la fecha',
|
|
[$post['body']['accion'] ?? null, $post['body']['fecha'] ?? null], ['apertura', $hoy]);
|
|
|
|
// ════ 5. Mantenimiento: requires/resolver + payload completo ═════════════════
|
|
echo "\nMantenimiento — el grupo se pide primero\n";
|
|
|
|
$ctx = contexto('57300MANT');
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_mantenimiento');
|
|
check('sin grupo elegido desvía al selector', isset(filasDe($r)['5']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '5');
|
|
check('y vuelve al flujo: pide la fecha', str_contains(textoDe($r), 'De qué fecha'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
|
check('el lote muestra la cantidad faltante', str_contains(filasDe($r)['4'] ?? '', 'falta 12'));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '4');
|
|
check('la labor sale del grupo elegido', filasDe($r), ['77' => 'PLATEO MANUAL']);
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '77');
|
|
$r = NormalBot::processInteractive($co, $ctx, '412');
|
|
$r = NormalBot::processInteractive($co, $ctx, '__listo');
|
|
$r = NormalBot::process($co, $ctx, '12');
|
|
check('resumen', isset(filasDe($r)['__cap_confirm']));
|
|
|
|
capturas();
|
|
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
|
$post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'labores_up'))[0] ?? null;
|
|
check('postea todo lo que labores_up exige',
|
|
[$post['body']['novedad_id'] ?? null, $post['body']['lote_id'] ?? null,
|
|
$post['body']['empleados'] ?? null, $post['body']['cantidad'] ?? null],
|
|
['77', '4', ['412'], '12']);
|
|
|
|
// ════ 6. Labores fase 2 ═══════════════════════════════════════════════════════
|
|
echo "\nLabores diarias — cuadrilla completa\n";
|
|
|
|
$ctx = contexto('57300LABOR');
|
|
$id = ConversationContext::getOrCreate(1, '57300LABOR')['id'];
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, 'registrar_labor');
|
|
$r = NormalBot::processInteractive($co, $ctx, $hoy);
|
|
check('el catálogo viene filtrado por fase', filasDe($r), ['42' => 'TRACTORISTA', '43' => 'HORAS EXTRA']);
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '42');
|
|
check('los lotes son los de su finca', isset(filasDe($r)['4']) && isset(filasDe($r)['9']));
|
|
check('sin lotes de otras fincas', !isset(filasDe($r)['99']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '4');
|
|
$r = NormalBot::processInteractive($co, $ctx, '412');
|
|
$r = NormalBot::processInteractive($co, $ctx, '415');
|
|
$r = NormalBot::processInteractive($co, $ctx, '__listo');
|
|
$r = NormalBot::process($co, $ctx, '1');
|
|
capturas();
|
|
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
|
$post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'labores_up'))[0] ?? null;
|
|
check('la cuadrilla viaja como arreglo', $post['body']['empleados'] ?? null, ['412', '415']);
|
|
|
|
// ════ 7. NLU: entity resuelve el grupo y ejecuta el informe ═══════════════════
|
|
echo "\nNLU — \"informe de mantenimiento de plateo\" en un mensaje\n";
|
|
|
|
$ctx = contexto('57300NLU');
|
|
$id = ConversationContext::getOrCreate(1, '57300NLU')['id'];
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
ConversationContext::updateNode($id, '__greeted');
|
|
|
|
AiBot::$respuestas = [[
|
|
'action' => 'route', 'key' => 'ciclo_mantenimiento_top',
|
|
'entities' => ['grupo' => 'plateo'],
|
|
]];
|
|
capturas();
|
|
$r = NormalBot::process($co, $ctx, 'quiero informe de mantenimiento de plateo');
|
|
check('el informe llegó sin menús intermedios', str_contains(textoDe($r), 'informe-de-prueba'));
|
|
|
|
$get = array_values(array_filter(capturas(), fn($c) => str_contains($c['peticion'] ?? '', 'mantenimiento_top')))[0] ?? null;
|
|
check('la entity resolvió el grupo en la URL', $get['query']['grupo'] ?? null, '5');
|
|
check('y la consulta dice quién pregunta', $get['query']['wa'] ?? null, '57300NLU');
|
|
|
|
// ════ 8. NLU coherente con los módulos del perfil ═════════════════════════════
|
|
echo "\nNLU — respeta los módulos habilitados\n";
|
|
|
|
// Este número solo puede cargar pluviometría
|
|
fakePhone('57300SOLOPLUVIO', null, 0);
|
|
FakeDb::$phones['57300SOLOPLUVIO']['modulos_json'] = json_encode(['carga' => ['pluviometria']]);
|
|
|
|
$ctx = contexto('57300SOLOPLUVIO');
|
|
$id = ConversationContext::getOrCreate(1, '57300SOLOPLUVIO')['id'];
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
ConversationContext::updateNode($id, '__greeted');
|
|
|
|
// "subir pluviometría" → directo a la fecha, un solo mensaje
|
|
AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_pluviometria']];
|
|
$r = NormalBot::process($co, $ctx, 'subir pluviometría');
|
|
check('"subir pluviometría" va de una a la fecha', str_contains(textoDe($r), 'De qué fecha'));
|
|
check('el NLU recibió su alcance de módulos', AiBot::$modulosRecibidos, ['pluviometria']);
|
|
|
|
// El modelo devuelve una clave fuera del alcance: el guard la corta
|
|
ConversationContext::updateNode($id, '__greeted');
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
WhatsAppSender::$enviados = [];
|
|
AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_ausentismo']];
|
|
$r = NormalBot::process($co, $ctx, 'registrar ausentismo');
|
|
check('un módulo vetado no ejecuta el flujo', !str_contains(textoDe($r), 'nombre o documento'));
|
|
check('avisa que no entendió',
|
|
str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', 'No estoy seguro'));
|
|
|
|
// Sin restricción, "subir labores diarias" también entra directo
|
|
$ctx = contexto('57300LABORNLU');
|
|
$id = ConversationContext::getOrCreate(1, '57300LABORNLU')['id'];
|
|
ConversationContext::updateMetadata($id, ['finca' => ['finca_id' => '4', 'finca_label' => 'REPOSO']]);
|
|
ConversationContext::updateNode($id, '__greeted');
|
|
AiBot::$respuestas = [['action' => 'route', 'key' => 'registrar_labor']];
|
|
$r = NormalBot::process($co, $ctx, 'subir labores diarias');
|
|
check('"subir labores diarias" arranca el flujo directo', str_contains(textoDe($r), 'De qué fecha es la labor'));
|
|
capturas();
|
|
|
|
// ════ Resultado ═══════════════════════════════════════════════════════════════
|
|
echo "\n" . ($GLOBALS['fallas'] ? "{$GLOBALS['fallas']} falla(s)\n" : "Todos los flujos ejecutan de punta a punta\n");
|
|
exit($GLOBALS['fallas'] ? 1 : 0);
|