Skip to content

Commit

Permalink
add monitor hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
claytercek committed Nov 5, 2024
1 parent 0cf2d56 commit b5cf452
Show file tree
Hide file tree
Showing 10 changed files with 567 additions and 148 deletions.
4 changes: 2 additions & 2 deletions packages/cli/lib/commands/stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { LogManager } from '@bluecadet/launchpad-utils';
* @param {import("../cli.js").LaunchpadArgv} argv
*/
export async function stop(argv) {
LogManager.configureRootLogger();
await LaunchpadMonitor.kill();
const logger = LogManager.configureRootLogger();
await LaunchpadMonitor.kill(logger);
}
2 changes: 1 addition & 1 deletion packages/content/lib/__tests__/launchpad-content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('LaunchpadContent', () => {

describe('constructor', () => {
it('should initialize with default options when no config provided', () => {
const content = new LaunchpadContent(undefined, createMockLogger());
const content = new LaunchpadContent({}, createMockLogger());
expect(content).toBeInstanceOf(LaunchpadContent);
expect(content._config).toEqual(resolveContentConfig());
});
Expand Down
92 changes: 87 additions & 5 deletions packages/monitor/lib/__tests__/launchpad-monitor.test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { LaunchpadMonitor } from '../launchpad-monitor.js';
import { createMockLogger } from '@bluecadet/launchpad-testing/test-utils.js';
import { ok, okAsync } from 'neverthrow';
import { okAsync } from 'neverthrow';
import { LogManager } from '@bluecadet/launchpad-utils';

// Mock process.exit to prevent tests from actually exiting
// @ts-expect-error - mockImplementation returns undefined
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);

/** @type {import('../core/monitor-plugin-driver.js').MonitorPlugin} */
const mockPlugin = {
name: 'test-plugin',
hooks: {
beforeConnect: vi.fn(),
afterConnect: vi.fn(),
beforeDisconnect: vi.fn(),
afterDisconnect: vi.fn(),
beforeAppStart: vi.fn(),
afterAppStart: vi.fn(),
beforeAppStop: vi.fn(),
afterAppStop: vi.fn(),
onAppError: vi.fn(),
onAppLog: vi.fn(),
onAppErrorLog: vi.fn(),
beforeShutdown: vi.fn()
}
};

function createTestMonitor(config = {
apps: [{
pm2: {
name: 'test-app',
script: 'test.js'
}
}]
}],
plugins: [mockPlugin]
}) {
const rootLogger = createMockLogger();
const monitor = new LaunchpadMonitor(config, rootLogger);
Expand All @@ -26,7 +47,8 @@ function createTestMonitor(config = {
return {
monitor,
rootLogger,
monitorLogger
monitorLogger,
plugin: config.plugins[0]
};
}

Expand All @@ -46,19 +68,28 @@ describe('LaunchpadMonitor', () => {
expect(monitor._busManager.connect).toHaveBeenCalled();
});

it('should run connect hooks in order', async () => {
const { monitor, plugin } = createTestMonitor();

await monitor.connect();

expect(plugin.hooks.beforeConnect).toHaveBeenCalled();
expect(plugin.hooks.afterConnect).toHaveBeenCalled();
});

it('should handle existing daemon when deleteExistingBeforeConnect is true', async () => {
const { monitor } = createTestMonitor();

monitor._config.deleteExistingBeforeConnect = true;
monitor._processManager.isDaemonRunning = vi.fn().mockImplementationOnce(() => okAsync(true));
vi.spyOn(monitor._processManager, 'killPm2');
const killSpy = vi.spyOn(LaunchpadMonitor, 'kill');
vi.spyOn(monitor._processManager, 'deleteAllProcesses');

const result = await monitor.connect();

expect(result).toBeOk();
expect(monitor._processManager.deleteAllProcesses).toHaveBeenCalled();
expect(monitor._processManager.killPm2).toHaveBeenCalled();
expect(killSpy).toHaveBeenCalled();
});
});

Expand All @@ -76,6 +107,15 @@ describe('LaunchpadMonitor', () => {
expect(monitor._busManager.disconnect).toHaveBeenCalled();
expect(monitor._processManager.disconnect).toHaveBeenCalled();
});

it('should run disconnect hooks in order', async () => {
const { monitor, plugin } = createTestMonitor();

await monitor.disconnect();

expect(plugin.hooks.beforeDisconnect).toHaveBeenCalled();
expect(plugin.hooks.afterDisconnect).toHaveBeenCalled();
});
});

describe('start', () => {
Expand Down Expand Up @@ -113,6 +153,21 @@ describe('LaunchpadMonitor', () => {
expect(result).toBeOk();
expect(monitor._appManager.startApp).toHaveBeenCalledWith('test-app');
});

it('should run app start hooks in order', async () => {
const { monitor, plugin } = createTestMonitor();

await monitor.start('test-app');

expect(plugin.hooks.beforeAppStart).toHaveBeenCalledWith(
expect.any(Object),
{ appName: 'test-app' }
);
expect(plugin.hooks.afterAppStart).toHaveBeenCalledWith(
expect.any(Object),
{ appName: 'test-app', process: expect.any(Object) }
);
});
});

describe('stop', () => {
Expand All @@ -126,6 +181,21 @@ describe('LaunchpadMonitor', () => {
expect(result).toBeOk();
expect(monitor._appManager.stopApp).toHaveBeenCalledWith('test-app');
});

it('should run app stop hooks in order', async () => {
const { monitor, plugin } = createTestMonitor();

await monitor.stop('test-app');

expect(plugin.hooks.beforeAppStop).toHaveBeenCalledWith(
expect.any(Object),
{ appName: 'test-app' }
);
expect(plugin.hooks.afterAppStop).toHaveBeenCalledWith(
expect.any(Object),
{ appName: 'test-app' }
);
});
});

describe('shutdown', () => {
Expand Down Expand Up @@ -158,5 +228,17 @@ describe('LaunchpadMonitor', () => {
await monitor.shutdown(123);
expect(mockExit).toHaveBeenCalledWith(123);
});

it('should run shutdown hook before stopping', async () => {
const { monitor, plugin } = createTestMonitor();

const exitCode = 123;
await monitor.shutdown(exitCode);

expect(plugin.hooks.beforeShutdown).toHaveBeenCalledWith(
expect.any(Object),
{ code: exitCode }
);
});
});
});
1 change: 0 additions & 1 deletion packages/monitor/lib/core/__tests__/app-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ function setupTestAppManager() {
}));
vi.spyOn(processManager, 'connect');
vi.spyOn(processManager, 'disconnect');
vi.spyOn(processManager, 'killPm2');
vi.spyOn(processManager, 'isDaemonRunning');
vi.spyOn(processManager, 'getProcesses');
vi.spyOn(processManager, 'deleteProcess');
Expand Down
113 changes: 91 additions & 22 deletions packages/monitor/lib/core/__tests__/monitor-plugin-driver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,113 @@ describe('MonitorPluginDriver', () => {
let monitorPluginDriver;
/** @type {import('@bluecadet/launchpad-utils').Logger} */
let mockLogger;
/** @type {import('@bluecadet/launchpad-utils/lib/plugin-driver.js').Plugin<import('../monitor-plugin-driver.js').MonitorHooks>[]} */
let mockPlugins;
/** @type {import('../monitor-plugin-driver.js').MonitorPlugin} */
let mockPlugin;
/** @type {import('../../launchpad-monitor.js').LaunchpadMonitor} */
let mockMonitor;

beforeEach(() => {
mockLogger = createMockLogger();
mockPlugin = {
name: 'test-plugin',
hooks: {
beforeConnect: vi.fn(),
afterConnect: vi.fn(),
beforeDisconnect: vi.fn(),
afterDisconnect: vi.fn(),
beforeAppStart: vi.fn(),
afterAppStart: vi.fn(),
beforeAppStop: vi.fn(),
afterAppStop: vi.fn(),
onAppError: vi.fn(),
onAppLog: vi.fn(),
onAppErrorLog: vi.fn(),
beforeShutdown: vi.fn()
}
};

mockPlugins = [
{
name: 'test-plugin',
hooks: {
tempMonitorHook: vi.fn()
}
mockMonitor = {
_logger: mockLogger,
// @ts-expect-error
_busManager: {
addEventHandler: vi.fn()
}
];
};

const basePluginDriver = new PluginDriver(mockLogger, mockPlugins);
monitorPluginDriver = new MonitorPluginDriver(basePluginDriver);
const basePluginDriver = new PluginDriver(mockLogger, [mockPlugin]);
monitorPluginDriver = new MonitorPluginDriver(basePluginDriver, { monitor: mockMonitor });
});

describe('constructor', () => {
it('should register bus event handler', () => {
expect(mockMonitor._busManager.addEventHandler).toHaveBeenCalledWith(
expect.any(Function)
);
});
});

describe('_getPluginContext', () => {
it('should return the expected plugin context', () => {
it('should return context with monitor instance', () => {
const context = monitorPluginDriver._getPluginContext();
expect(context).toEqual({
foo: 'lorem'
monitor: mockMonitor
});
});
});

describe('plugin hooks', () => {
it('should call plugin hooks with correct context', async () => {
const plugin = mockPlugins[0];
await monitorPluginDriver.runHookSequential('tempMonitorHook');

expect(plugin.hooks.tempMonitorHook).toHaveBeenCalledWith(
expect.objectContaining({
foo: 'lorem'
})
describe('_handleBusEvent', () => {
it('should handle process error events', () => {
monitorPluginDriver._handleBusEvent('process:event', {
process: { name: 'test-app' },
event: 'error',
data: 'test error'
});

expect(mockPlugin.hooks.onAppError).toHaveBeenCalledWith(
expect.any(Object),
{
appName: 'test-app',
error: expect.any(Error)
}
);
});

it('should handle stdout log events', () => {
monitorPluginDriver._handleBusEvent('log:out', {
process: { name: 'test-app' },
data: 'test log'
});

expect(mockPlugin.hooks.onAppLog).toHaveBeenCalledWith(
expect.any(Object),
{
appName: 'test-app',
data: 'test log'
}
);
});

it('should handle stderr log events', () => {
monitorPluginDriver._handleBusEvent('log:err', {
process: { name: 'test-app' },
data: 'test error log'
});

expect(mockPlugin.hooks.onAppErrorLog).toHaveBeenCalledWith(
expect.any(Object),
{
appName: 'test-app',
data: 'test error log'
}
);
});

it('should ignore events without process name', () => {
monitorPluginDriver._handleBusEvent('log:out', {
data: 'test log'
});

expect(mockPlugin.hooks.onAppLog).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit b5cf452

Please sign in to comment.