El servidor no trae el driver de SQLite y el arnes reventaba al arrancar. NormalBot solo hace dos consultas —el endpoint por clave y el perfil por numero— asi que dos mapas en memoria alcanzan y el arnes deja de depender de cualquier driver de PDO. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
243 lines
8.6 KiB
PHP
243 lines
8.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Arnés para ejecutar NormalBot de verdad, sin MySQL ni WhatsApp:
|
|
*
|
|
* db() -> falso en PHP puro (sin drivers), con los endpoints
|
|
* del seed reescritos hacia el servidor de fixtures
|
|
* ConversationContext -> estado en memoria, un contexto por teléfono
|
|
* WhatsAppSender -> captura los envíos directos en ::$enviados
|
|
* AiBot -> respuestas guionadas en ::$respuestas
|
|
*
|
|
* El curl es real: pega contra fixtures_api.php servido con `php -S`.
|
|
*/
|
|
|
|
const FIXTURE_BASE = 'http://127.0.0.1:8973/fixtures_api.php';
|
|
|
|
// ── db(): falso en PHP puro ──────────────────────────────────────────────────
|
|
// Sin SQLite: los servidores no siempre traen pdo_sqlite. NormalBot solo hace
|
|
// dos consultas —el endpoint por clave y el perfil por número— así que un par
|
|
// de mapas en memoria alcanza y no dependemos de ningún driver.
|
|
|
|
class FakeStmt
|
|
{
|
|
private $row = false;
|
|
public function __construct(private string $sql) {}
|
|
|
|
public function execute(array $p = []): bool
|
|
{
|
|
if (str_contains($this->sql, 'company_endpoints')) {
|
|
$key = $p[1] ?? '';
|
|
$url = FakeDb::$endpoints[$key] ?? null;
|
|
$this->row = $url === null ? false
|
|
: ['url' => $url, 'method' => 'GET', 'params' => null];
|
|
} elseif (str_contains($this->sql, 'company_phones')) {
|
|
$this->row = FakeDb::$phones[$p[1] ?? ''] ?? false;
|
|
} else {
|
|
$this->row = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function fetch() { return $this->row; }
|
|
public function fetchColumn() { return $this->row ? array_values($this->row)[0] : false; }
|
|
public function fetchAll(): array { return $this->row ? [$this->row] : []; }
|
|
}
|
|
|
|
class FakeDb
|
|
{
|
|
/** endpoint_key => url absoluta hacia el servidor de fixtures */
|
|
public static array $endpoints = [];
|
|
/** wa_number => fila de company_phones */
|
|
public static array $phones = [];
|
|
|
|
public function prepare(string $sql): FakeStmt { return new FakeStmt($sql); }
|
|
public function query(string $sql): FakeStmt { $s = new FakeStmt($sql); $s->execute(); return $s; }
|
|
public function exec(string $sql): int { return 0; }
|
|
}
|
|
|
|
function db(): FakeDb
|
|
{
|
|
static $db = null;
|
|
if ($db !== null) return $db;
|
|
|
|
$db = new FakeDb();
|
|
// Los endpoints salen del seed real: mismas claves y placeholders, otra base.
|
|
$seed = file_get_contents(dirname(__DIR__) . '/seed_palmas.php');
|
|
preg_match_all("/\['key' => '([^']+)',\s*'dir' => '([^']+)',\s*'url' => \\\$BASE \. '([^']*)'/", $seed, $m);
|
|
foreach ($m[1] as $i => $key) {
|
|
FakeDb::$endpoints[$key] = FIXTURE_BASE . $m[3][$i];
|
|
}
|
|
return $db;
|
|
}
|
|
|
|
/** Registra un número con perfil para los escenarios de trabajador/supervisor. */
|
|
function fakePhone(string $wa, ?int $tercero, int $esSupervisor): void
|
|
{
|
|
FakeDb::$phones[$wa] = [
|
|
'tercero_id' => $tercero,
|
|
'modulos_json' => null,
|
|
'es_supervisor' => $esSupervisor,
|
|
];
|
|
}
|
|
|
|
// ── ConversationContext en memoria ───────────────────────────────────────────
|
|
class ConversationContext
|
|
{
|
|
public static array $store = [];
|
|
|
|
private static function &ctx(int $id): array
|
|
{
|
|
if (!isset(self::$store[$id])) {
|
|
self::$store[$id] = ['current_node' => null, 'metadata' => []];
|
|
}
|
|
return self::$store[$id];
|
|
}
|
|
|
|
public static function getOrCreate(int $companyId, string $phone, string $botType = 'normal'): array
|
|
{
|
|
// Un id estable por teléfono para aislar escenarios
|
|
$id = crc32($companyId . '|' . $phone) % 100000;
|
|
$c = self::ctx($id);
|
|
return ['id' => $id, 'current_node' => $c['current_node'], 'metadata' => json_encode($c['metadata'])];
|
|
}
|
|
|
|
public static function updateNode(int $id, ?string $node): void
|
|
{
|
|
self::ctx($id)['current_node'] = $node;
|
|
}
|
|
|
|
public static function getMetadata(int $id): array
|
|
{
|
|
return self::ctx($id)['metadata'];
|
|
}
|
|
|
|
public static function updateMetadata(int $id, array $meta): void
|
|
{
|
|
self::ctx($id)['metadata'] = $meta;
|
|
}
|
|
|
|
public static function reset(int $id): void
|
|
{
|
|
self::$store[$id] = ['current_node' => null, 'metadata' => []];
|
|
}
|
|
}
|
|
|
|
// ── WhatsAppSender: captura ──────────────────────────────────────────────────
|
|
class WhatsAppSender
|
|
{
|
|
public static array $enviados = [];
|
|
|
|
public static function sendText(string $to, string $text, string $phoneNumberId): array
|
|
{
|
|
self::$enviados[] = ['to' => $to, 'text' => $text];
|
|
return ['success' => true];
|
|
}
|
|
|
|
public static function uploadMedia(string $file, string $mime, string $phoneNumberId): array
|
|
{
|
|
return ['success' => true, 'media_id' => 'test-media'];
|
|
}
|
|
|
|
public static function sendDocument(string $to, string $mediaId, string $phoneNumberId, string $caption, string $name): array
|
|
{
|
|
self::$enviados[] = ['to' => $to, 'document' => $name];
|
|
return ['success' => true];
|
|
}
|
|
}
|
|
|
|
// ── AiBot guionado ───────────────────────────────────────────────────────────
|
|
class AiBot
|
|
{
|
|
/** Cola de respuestas para routeOrChat; vacía = chat vacío (cae al fallback). */
|
|
public static array $respuestas = [];
|
|
|
|
public static function routeOrChat(array $company, array $context, string $input): array
|
|
{
|
|
return array_shift(self::$respuestas) ?? ['action' => 'chat', 'text' => ''];
|
|
}
|
|
}
|
|
|
|
require_once dirname(__DIR__, 2) . '/services/NormalBot.php';
|
|
|
|
// ── Utilidades del test ──────────────────────────────────────────────────────
|
|
|
|
function empresa(): array
|
|
{
|
|
static $config = null;
|
|
if ($config === null) {
|
|
$seed = file_get_contents(dirname(__DIR__) . '/seed_palmas.php');
|
|
$open = strpos($seed, '[', strpos($seed, '$configJson = ['));
|
|
$d = 0; $end = $open;
|
|
for ($i = $open, $n = strlen($seed); $i < $n; $i++) {
|
|
if ($seed[$i] === '[') $d++;
|
|
elseif ($seed[$i] === ']') { $d--; if (!$d) { $end = $i; break; } }
|
|
}
|
|
$config = eval('return ' . substr($seed, $open, $end - $open + 1) . ';');
|
|
// Sin saludo diario: mete un envío extra en cada primer mensaje
|
|
$config['welcome']['enabled'] = false;
|
|
}
|
|
return [
|
|
'id' => 1,
|
|
'name' => 'test',
|
|
'display_name' => 'Empresa Test',
|
|
'api_key' => 'k',
|
|
'config_json' => json_encode($config),
|
|
'_permission_type' => 3,
|
|
];
|
|
}
|
|
|
|
function contexto(string $phone): array
|
|
{
|
|
return ['from' => $phone, 'name' => 'Tester', 'phone_number_id' => 'pn', 'permission_type' => 3];
|
|
}
|
|
|
|
/** Texto plano de la respuesta del bot, venga como texto o interactivo. */
|
|
function textoDe(?array $resp): string
|
|
{
|
|
if (!$resp) return '';
|
|
$p = json_decode($resp['payload'] ?? '{}', true);
|
|
if (isset($p['text'])) return $p['text'];
|
|
$i = $p['interactive'] ?? [];
|
|
return ($i['header']['text'] ?? '') . ' ' . ($i['body']['text'] ?? '');
|
|
}
|
|
|
|
/** Filas/botones de una respuesta interactiva, como id => title. */
|
|
function filasDe(?array $resp): array
|
|
{
|
|
$p = json_decode($resp['payload'] ?? '{}', true);
|
|
$i = $p['interactive'] ?? [];
|
|
$out = [];
|
|
foreach ($i['action']['sections'] ?? [] as $sec) {
|
|
foreach ($sec['rows'] ?? [] as $r) $out[$r['id']] = $r['title'];
|
|
}
|
|
foreach ($i['action']['buttons'] ?? [] as $b) {
|
|
$out[$b['reply']['id']] = $b['reply']['title'];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/** Las capturas del servidor de fixtures desde la última llamada. */
|
|
function capturas(): array
|
|
{
|
|
$log = getenv('CAPTURE_LOG');
|
|
if (!is_file($log)) return [];
|
|
$out = [];
|
|
foreach (file($log, FILE_IGNORE_NEW_LINES) as $l) $out[] = json_decode($l, true);
|
|
file_put_contents($log, '');
|
|
return $out;
|
|
}
|
|
|
|
$GLOBALS['fallas'] = 0;
|
|
function check(string $nombre, $obtenido, $esperado = true): void
|
|
{
|
|
$ok = $obtenido === $esperado;
|
|
if (!$ok) $GLOBALS['fallas']++;
|
|
printf("%s %s\n", $ok ? ' ok ' : ' FALLA', $nombre);
|
|
if (!$ok) {
|
|
echo " esperaba: " . json_encode($esperado, JSON_UNESCAPED_UNICODE) . "\n";
|
|
echo " obtuvo: " . json_encode($obtenido, JSON_UNESCAPED_UNICODE) . "\n";
|
|
}
|
|
}
|