"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const events_1 = require("events");
const sinon = __importStar(require("sinon"));
const cluster_1 = __importDefault(require("cluster"));
const config_1 = __importDefault(require("../../../src/config"));
const logger_1 = __importDefault(require("../../../src/logger"));
const worker_1 = require("../../../src/services/worker");
const unit_1 = require("../../helpers/unit");
describe('Worker Service', function () {
    before(unit_1.unitBeforeHelper);
    after(unit_1.unitAfterHelper);
    describe('Worker Restart on Crash', function () {
        let sandbox;
        let clock;
        let mockWorkers;
        let forkStub;
        let loggerStubs;
        beforeEach(() => {
            sandbox = sinon.createSandbox();
            clock = sandbox.useFakeTimers({ shouldAdvanceTime: true });
            mockWorkers = [];
            // Stub logger methods
            loggerStubs = {
                verbose: sandbox.stub(logger_1.default, 'verbose'),
                info: sandbox.stub(logger_1.default, 'info'),
                error: sandbox.stub(logger_1.default, 'error'),
                warn: sandbox.stub(logger_1.default, 'warn')
            };
            // Stub cluster module
            sandbox.stub(cluster_1.default, 'isPrimary').value(true);
            forkStub = sandbox.stub(cluster_1.default, 'fork').callsFake(() => {
                const mockWorker = createMockWorker();
                mockWorkers.push(mockWorker);
                // Emit listening on next tick (but before fake timers block it)
                Promise.resolve().then(() => mockWorker.emit('listening'));
                return mockWorker;
            });
            // Mock config.numWorkers
            sandbox.stub(config_1.default, 'numWorkers').value(3);
            // Clear the Worker service's internal state
            worker_1.Worker.workers = [];
            worker_1.Worker.shuttingDown = false;
        });
        afterEach(() => {
            sandbox.restore();
        });
        // Helper function to create mock workers
        function createMockWorker() {
            const worker = new events_1.EventEmitter();
            worker.process = { pid: Math.floor(Math.random() * 10000) + 10000 };
            worker.isConnected = () => true;
            worker.disconnect = sandbox.stub();
            return worker;
        }
        it('should restart worker after 5 seconds on abnormal exit (non-zero code)', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            (0, chai_1.expect)(initialWorkerCount).to.equal(3);
            const crashedWorker = mockWorkers[0];
            const crashedPid = crashedWorker.process.pid;
            // Simulate worker crash
            crashedWorker.emit('exit', 1, null);
            // Verify immediate state
            (0, chai_1.expect)(loggerStubs.error.calledWith(sinon.match(`Worker ${crashedPid} crashed`))).to.be.true;
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('Scheduling worker 0 restart in 5 seconds'))).to.be.true;
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount); // Not restarted yet
            // Advance time but not quite 5 seconds
            clock.tick(4999);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount); // Still not restarted
            // Advance to exactly 5 seconds
            clock.tick(1);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 1); // Now restarted
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0 (restart #1)'))).to.be.true;
            // Verify the new worker has workerId 0
            const restartedWorker = mockWorkers[mockWorkers.length - 1];
            restartedWorker.emit('listening');
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match(`Worker 0 successfully restarted (pid: ${restartedWorker.process.pid})`))).to.be.true;
        });
        it('should restart on signal-terminated exit', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            const crashedWorker = mockWorkers[0];
            // Simulate worker killed by signal
            crashedWorker.emit('exit', null, 'SIGKILL');
            (0, chai_1.expect)(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true;
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('Scheduling worker 0 restart in 5 seconds'))).to.be.true;
            // Advance 5 seconds
            clock.tick(5000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 1);
        });
        it('should NOT restart on graceful exit (code 0)', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            const exitedWorker = mockWorkers[0];
            // Simulate graceful exit
            exitedWorker.emit('exit', 0, null);
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('stopped gracefully'))).to.be.true;
            // Advance time well past 5 seconds
            clock.tick(10000);
            // Should NOT have restarted
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount);
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('Scheduling'))).to.be.false;
        });
        it('should NOT restart during shutdown', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            // Initiate shutdown
            await worker_1.Worker.stop();
            (0, chai_1.expect)(worker_1.Worker.shuttingDown).to.be.true;
            // Simulate worker crash after shutdown initiated
            const crashedWorker = mockWorkers[0];
            crashedWorker.emit('exit', 1, null);
            // Advance time
            clock.tick(5000);
            // Should NOT have restarted
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount);
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('Not restarting worker'))).to.be.false;
        });
        it('should increment restart count correctly', async () => {
            await worker_1.Worker.start();
            // First crash
            mockWorkers[0].emit('exit', 1, null);
            clock.tick(5000);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('restart #1'))).to.be.true;
            // Get the restarted worker and crash it again
            const restartedWorker1 = mockWorkers[mockWorkers.length - 1];
            restartedWorker1.emit('listening');
            restartedWorker1.emit('exit', 1, null);
            clock.tick(5000);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('restart #2'))).to.be.true;
            // Crash again
            const restartedWorker2 = mockWorkers[mockWorkers.length - 1];
            restartedWorker2.emit('listening');
            restartedWorker2.emit('exit', 1, null);
            clock.tick(5000);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('restart #3'))).to.be.true;
        });
        it('should preserve worker ID across restarts', async () => {
            await worker_1.Worker.start();
            // Crash worker 1 (middle worker)
            const crashedWorker = mockWorkers[1];
            crashedWorker.emit('exit', 1, null);
            // Advance timer
            clock.tick(5000);
            // Verify restart message mentions worker 1, not worker 3
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 1'))).to.be.true;
            // Trigger listening event
            const restartedWorker = mockWorkers[mockWorkers.length - 1];
            restartedWorker.emit('listening');
            (0, chai_1.expect)(loggerStubs.info.calledWith(sinon.match('Worker 1 successfully restarted'))).to.be.true;
        });
        it('should restart multiple workers independently', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            // Crash worker 0 at t=0
            mockWorkers[0].emit('exit', 1, null);
            // Crash worker 2 at t=2000ms
            clock.tick(2000);
            mockWorkers[2].emit('exit', 1, null);
            // Advance to t=5000ms - worker 0 should restart
            clock.tick(3000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 1);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true;
            // Worker 2 shouldn't have restarted yet
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 2'))).to.be.false;
            // Advance to t=7000ms - worker 2 should restart
            clock.tick(2000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 2);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 2'))).to.be.true;
        });
        it('should re-attach event handlers on restart', async () => {
            await worker_1.Worker.start();
            // Crash a worker
            mockWorkers[0].emit('exit', 1, null);
            clock.tick(5000);
            // Get the restarted worker
            const restartedWorker = mockWorkers[mockWorkers.length - 1];
            // Verify it has event listeners
            (0, chai_1.expect)(restartedWorker.listenerCount('exit')).to.be.greaterThan(0);
            (0, chai_1.expect)(restartedWorker.listenerCount('message')).to.be.greaterThan(0);
            (0, chai_1.expect)(restartedWorker.listenerCount('listening')).to.be.greaterThan(0);
            // Test that exit handler still works
            restartedWorker.emit('listening');
            restartedWorker.emit('exit', 1, null);
            clock.tick(5000);
            // Should have restarted again
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('restart #2'))).to.be.true;
        });
        it('should log restart information correctly', async () => {
            await worker_1.Worker.start();
            const crashedWorker = mockWorkers[0];
            const crashedPid = crashedWorker.process.pid;
            // Crash the worker
            crashedWorker.emit('exit', 1, null);
            // Verify crash log
            (0, chai_1.expect)(loggerStubs.error.calledWith(sinon.match(`Worker ${crashedPid} crashed (code: 1, signal: null)`))).to.be.true;
            // Verify scheduling log
            (0, chai_1.expect)(loggerStubs.info.calledWith('Scheduling worker 0 restart in 5 seconds...')).to.be.true;
            // Advance timer
            clock.tick(5000);
            // Verify restart log
            (0, chai_1.expect)(loggerStubs.warn.calledWith('Restarting worker 0 (restart #1)')).to.be.true;
            // Emit listening event
            const restartedWorker = mockWorkers[mockWorkers.length - 1];
            const newPid = restartedWorker.process.pid;
            restartedWorker.emit('listening');
            // Verify success log
            (0, chai_1.expect)(loggerStubs.info.calledWith(`Worker 0 successfully restarted (pid: ${newPid})`)).to.be.true;
        });
        it('should disconnect all workers on stop()', async () => {
            await worker_1.Worker.start();
            const worker0 = mockWorkers[0];
            const worker1 = mockWorkers[1];
            const worker2 = mockWorkers[2];
            // Call stop
            await worker_1.Worker.stop();
            // Verify shuttingDown flag
            (0, chai_1.expect)(worker_1.Worker.shuttingDown).to.be.true;
            // Verify disconnect called on all workers
            (0, chai_1.expect)(worker0.disconnect.called).to.be.true;
            (0, chai_1.expect)(worker1.disconnect.called).to.be.true;
            (0, chai_1.expect)(worker2.disconnect.called).to.be.true;
            // Verify no restarts happen after stop
            worker0.emit('exit', 1, null);
            clock.tick(5000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(3); // Still just the initial 3
        });
        it('should remove worker from array on exit', async () => {
            await worker_1.Worker.start();
            // Initial count
            (0, chai_1.expect)(worker_1.Worker.workerCount()).to.equal(3);
            // Crash a worker
            mockWorkers[1].emit('exit', 1, null);
            // Should immediately be removed
            (0, chai_1.expect)(worker_1.Worker.workerCount()).to.equal(2);
            // Advance timer to restart
            clock.tick(5000);
            // After listening event, count should be back to 3
            const restartedWorker = mockWorkers[mockWorkers.length - 1];
            restartedWorker.emit('listening');
            (0, chai_1.expect)(worker_1.Worker.workerCount()).to.equal(3);
        });
        it('should restart on OOM (exit code 137)', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            // Simulate OOM
            mockWorkers[0].emit('exit', 137, null);
            (0, chai_1.expect)(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true;
            // Advance 5 seconds
            clock.tick(5000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 1);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true;
        });
        it('should restart on uncaught exception (exit code 1)', async () => {
            await worker_1.Worker.start();
            const initialWorkerCount = forkStub.callCount;
            // Simulate uncaught exception
            mockWorkers[0].emit('exit', 1, null);
            (0, chai_1.expect)(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true;
            // Advance 5 seconds
            clock.tick(5000);
            (0, chai_1.expect)(forkStub.callCount).to.equal(initialWorkerCount + 1);
            (0, chai_1.expect)(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true;
        });
    });
});
//# sourceMappingURL=worker.test.js.map