'use strict'; // Heartbeat-Tests für TelnetSenderGRBL: // Erkennung toter Verbindungen nach NotAus / Stromausfall beim Roboter. const { EventEmitter } = require('events'); const TelnetSenderGRBL = require('../robot/TelnetSenderGRBL'); // ── Hilfsfunktionen ───────────────────────────────────────────────────────── function makeRawSocket() { const em = new EventEmitter(); return Object.assign(em, { write: jest.fn(), end: jest.fn(), destroy: jest.fn(), setKeepAlive: jest.fn(), }); } function makeTelnetSocket() { const em = new EventEmitter(); return Object.assign(em, { write: jest.fn(), end: jest.fn(), destroy: jest.fn(), }); } /** * Erstellt einen Sender mit gemockten Abhängigkeiten und simuliert * einen erfolgreichen Verbindungsaufbau. */ function setup({ heartbeatInterval = 1000, deadTimeout = 5000 } = {}) { const rawSocket = makeRawSocket(); const telnetSock = makeTelnetSocket(); // Intervall-Verwaltung: injiziert, damit kein echter setInterval läuft. const activeIntervals = new Map(); let nextId = 1; const setIntervalFn = jest.fn((cb) => { const id = nextId++; activeIntervals.set(id, cb); return id; }); const clearIntervalFn = jest.fn((id) => { activeIntervals.delete(id); }); // Timeout-Mock: verhindert echte setTimeout-Aufrufe bei Reconnect-Scheduling. const setTimeoutFn = jest.fn(() => 99); const clearTimeoutFn = jest.fn(); const sender = new TelnetSenderGRBL( 'robot.local', 5000, 'x', 'y', 'z', null, null, null, null, { netModule: { createConnection: jest.fn(() => rawSocket) }, TelnetSocketClass: jest.fn(() => telnetSock), setIntervalFn, clearIntervalFn, setTimeoutFn, clearTimeoutFn, heartbeatInterval, deadTimeout, autoConnect: false, } ); // Verbindungsaufbau simulieren sender.connect(); rawSocket.emit('connect'); /** Heartbeat-Callback manuell auslösen. */ const fireHeartbeat = () => { const entries = [...activeIntervals.values()]; if (entries.length > 0) entries[0](); }; return { sender, rawSocket, telnetSock, activeIntervals, fireHeartbeat }; } // ── Tests ──────────────────────────────────────────────────────────────────── describe('TelnetSenderGRBL — Heartbeat / NotAus-Erkennung', () => { test('TCP-Keepalive wird beim Verbinden aktiviert', () => { const { rawSocket } = setup(); expect(rawSocket.setKeepAlive).toHaveBeenCalledWith(true, 2000); }); test('Heartbeat-Timer wird nach Verbindung gestartet', () => { const { activeIntervals } = setup(); expect(activeIntervals.size).toBe(1); }); test('Heartbeat sendet ? an den Controller', () => { const { telnetSock, fireHeartbeat } = setup(); fireHeartbeat(); expect(telnetSock.write).toHaveBeenCalledWith('?'); }); test('Socket wird zerstört wenn deadTimeout überschritten', () => { const { sender, rawSocket, fireHeartbeat } = setup({ deadTimeout: 5000 }); sender._lastDataAt = Date.now() - 20_000; // älter als deadTimeout → tot fireHeartbeat(); expect(rawSocket.destroy).toHaveBeenCalled(); }); test('Heartbeat-Timer wird nach Timeout-Erkennung gestoppt', () => { const { sender, activeIntervals, fireHeartbeat } = setup({ deadTimeout: 5000 }); sender._lastDataAt = Date.now() - 20_000; fireHeartbeat(); expect(activeIntervals.size).toBe(0); }); test('Kein Destroy wenn Daten innerhalb deadTimeout empfangen wurden', () => { const { sender, rawSocket, telnetSock, fireHeartbeat } = setup({ deadTimeout: 5000 }); sender._lastDataAt = Date.now(); // soeben empfangen → kein Timeout fireHeartbeat(); expect(rawSocket.destroy).not.toHaveBeenCalled(); expect(telnetSock.write).toHaveBeenCalledWith('?'); }); test('Eingehende Daten aktualisieren _lastDataAt', () => { const { sender, rawSocket } = setup(); const before = sender._lastDataAt; rawSocket.emit('data', Buffer.from('')); expect(sender._lastDataAt).toBeGreaterThanOrEqual(before); }); test('Heartbeat-Timer wird bei socket close gestoppt', () => { const { activeIntervals, telnetSock } = setup(); expect(activeIntervals.size).toBe(1); telnetSock.emit('close'); expect(activeIntervals.size).toBe(0); }); test('Heartbeat-Timer wird bei disconnect() gestoppt', () => { const { sender, activeIntervals } = setup(); expect(activeIntervals.size).toBe(1); sender.disconnect(); expect(activeIntervals.size).toBe(0); }); test('Heartbeat stoppt sich selbst wenn tSocket null ist', () => { const { sender, rawSocket, telnetSock, activeIntervals, fireHeartbeat } = setup(); sender.tSocket = null; fireHeartbeat(); expect(rawSocket.destroy).not.toHaveBeenCalled(); expect(telnetSock.write).not.toHaveBeenCalled(); expect(activeIntervals.size).toBe(0); }); });