Cada toque redibuja la lista en un mensaje nuevo: seis lotes eran seis mensajes. Ahora se puede escribir "1a, 2b 5c" y quedan marcados de una. Suma a lo ya tocado en vez de reemplazarlo, asi las dos formas conviven. Compara sin acentos ni mayusculas y acepta el lote solo o con la finca delante. Si dos fincas tienen un "1A" no adivina: exige el nombre completo. Y lo que no reconoce lo dice —marcar cinco de seis en silencio registraria un ciclo de menos sin que nadie se entere. Se separa por comas y se prueba el segmento entero antes de partirlo por espacios: al reves, "RETIRO 1A" se rompia en dos y no matcheaba nada. Descartado "marcar todos": nunca se abren todos los lotes de una finca. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
343 lines
17 KiB
PHP
343 lines
17 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 tipo, no los 17 motivos de una',
|
|
array_keys($filas), ['Incapacidad', 'Permiso', 'Vacaciones']);
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, 'Incapacidad');
|
|
$filas = filasDe($r);
|
|
check('el motivo se acota al tipo elegido', array_map('strval', array_keys($filas)), ['46', '47']);
|
|
check('sin el prefijo del grupo, entra en los 24 de WhatsApp',
|
|
max(array_map('mb_strlen', $filas)) <= 24);
|
|
$p = json_decode($r['payload'], true);
|
|
$fila0 = $p['interactive']['action']['sections'][0]['rows'][0];
|
|
check('y el nombre completo va en la descripción',
|
|
$fila0['description'] ?? null, 'INCAPACIDAD ENFERMEDAD < 3 DIAS');
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '46');
|
|
check('sigue la fecha inicial', str_contains(textoDe($r), 'Desde qué fecha'));
|
|
check('la fecha inicial sí mira hacia atrás',
|
|
isset(filasDe($r)[date('Y-m-d', strtotime('-1 day'))]));
|
|
|
|
$hoy = date('Y-m-d'); $ayer = date('Y-m-d', strtotime('-1 day'));
|
|
$manana = 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'));
|
|
|
|
// Una novedad termina hoy o despues: los atajos van hacia adelante
|
|
$atajos = filasDe($r);
|
|
check('ofrece hoy, mañana y pasado — no ayer',
|
|
array_map('strval', array_keys($atajos)),
|
|
[$hoy, $manana, date('Y-m-d', strtotime('+2 days')), '__cap_other']);
|
|
check('sin atajos hacia atrás', !isset($atajos[$ayer]));
|
|
|
|
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', '46', $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 tipo',
|
|
str_contains(textoDe($r), 'tipo de 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');
|
|
|
|
// Escribir varios de una: seis toques eran seis mensajes
|
|
$r = NormalBot::process($co, $ctx, '3a, 4a 5a');
|
|
$filas = filasDe($r);
|
|
check('escribir marca varios en un solo mensaje',
|
|
str_contains($filas['__listo'] ?? '', '(5)'));
|
|
// El contador no depende de la pagina que se este viendo
|
|
check('suma a lo tocado en vez de reemplazarlo: 2 + 3 = 5',
|
|
str_contains($filas['__listo'] ?? '', '(5)'));
|
|
|
|
WhatsAppSender::$enviados = [];
|
|
$r = NormalBot::process($co, $ctx, '6a, 9z9z');
|
|
check('lo que no reconoce lo avisa, no lo ignora',
|
|
str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', '"9z9z"'));
|
|
check('y marca igual lo que sí entendió',
|
|
str_contains(filasDe($r)['__listo'] ?? '', '(6)'));
|
|
|
|
$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 lleva los tocados y los escritos',
|
|
$post['body']['lote_ids'] ?? null, ['1', '2', '3', '4', '5', '6']);
|
|
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();
|
|
|
|
// ════ 9. Editar desde el resumen ══════════════════════════════════════════════
|
|
echo "\nEditar — corregir sin rehacer el formulario\n";
|
|
|
|
$ctx = contexto('57300EDITAR');
|
|
NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo');
|
|
NormalBot::process($co, $ctx, 'julio');
|
|
NormalBot::processInteractive($co, $ctx, '412');
|
|
NormalBot::processInteractive($co, $ctx, 'Incapacidad');
|
|
NormalBot::processInteractive($co, $ctx, '46');
|
|
NormalBot::processInteractive($co, $ctx, $hoy);
|
|
$r = NormalBot::processInteractive($co, $ctx, $manana);
|
|
check('el resumen ofrece editar', isset(filasDe($r)['__cap_edit']));
|
|
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_edit');
|
|
$campos = filasDe($r);
|
|
check('lista los campos ya cargados',
|
|
array_keys($campos),
|
|
['__cap_ed:empleado_id', '__cap_ed:grupo_novedad', '__cap_ed:novedad_id',
|
|
'__cap_ed:fecha_inicial', '__cap_ed:fecha_final', '__cap_ed_volver']);
|
|
|
|
// Un campo hoja: se corrige y vuelve derecho al resumen
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_ed:fecha_final');
|
|
check('pregunta solo ese campo', str_contains(textoDe($r), 'Hasta qué fecha'));
|
|
$pasado = date('Y-m-d', strtotime('+2 days'));
|
|
$r = NormalBot::processInteractive($co, $ctx, $pasado);
|
|
check('y vuelve al resumen, no al siguiente campo', isset(filasDe($r)['__cap_confirm']));
|
|
check('con el valor corregido', str_contains(textoDe($r), $pasado));
|
|
|
|
// Un campo del que otro depende: se re-pregunta en cadena
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_edit');
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_ed:grupo_novedad');
|
|
check('editar el tipo vuelve a preguntar el tipo', str_contains(textoDe($r), 'tipo de ausentismo'));
|
|
$r = NormalBot::processInteractive($co, $ctx, 'Permiso');
|
|
check('y encadena el motivo, que quedó inválido', str_contains(textoDe($r), 'Cuál es el motivo'));
|
|
check('acotado al tipo nuevo', array_map('strval', array_keys(filasDe($r))), ['50']);
|
|
$r = NormalBot::processInteractive($co, $ctx, '50');
|
|
check('recién ahí vuelve al resumen', isset(filasDe($r)['__cap_confirm']));
|
|
|
|
// Salir sin tocar nada
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_edit');
|
|
$r = NormalBot::processInteractive($co, $ctx, '__cap_ed_volver');
|
|
check('"Volver" deja todo como estaba', isset(filasDe($r)['__cap_confirm']));
|
|
|
|
capturas();
|
|
NormalBot::processInteractive($co, $ctx, '__cap_confirm');
|
|
$post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'ausentismos_up'))[0] ?? null;
|
|
check('el POST lleva lo editado, sin restos del valor viejo',
|
|
[$post['body']['novedad_id'] ?? null, $post['body']['fecha_final'] ?? null],
|
|
['50', $pasado]);
|
|
|
|
// ════ Resultado ═══════════════════════════════════════════════════════════════
|
|
echo "\n" . ($GLOBALS['fallas'] ? "{$GLOBALS['fallas']} falla(s)\n" : "Todos los flujos ejecutan de punta a punta\n");
|
|
exit($GLOBALS['fallas'] ? 1 : 0);
|