chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { SlashCommandPayload, SystemReloadPayload } from '@mosaicstack/types';
|
||||
import { ReloadService } from './reload.service.js';
|
||||
import { CommandExecutorService } from '../commands/command-executor.service.js';
|
||||
|
||||
function createMockCommandRegistry() {
|
||||
return {
|
||||
getManifest: vi.fn().mockReturnValue({
|
||||
version: 1,
|
||||
commands: [],
|
||||
skills: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createService() {
|
||||
const registry = createMockCommandRegistry();
|
||||
const service = new ReloadService(registry as never);
|
||||
return { service, registry };
|
||||
}
|
||||
|
||||
describe('ReloadService', () => {
|
||||
it('reload() calls onUnload then onLoad for registered MosaicPlugin', async () => {
|
||||
const { service } = createService();
|
||||
|
||||
const callOrder: string[] = [];
|
||||
const mockPlugin = {
|
||||
pluginName: 'test-plugin',
|
||||
onLoad: vi.fn().mockImplementation(() => {
|
||||
callOrder.push('onLoad');
|
||||
return Promise.resolve();
|
||||
}),
|
||||
onUnload: vi.fn().mockImplementation(() => {
|
||||
callOrder.push('onUnload');
|
||||
return Promise.resolve();
|
||||
}),
|
||||
};
|
||||
|
||||
service.registerPlugin('test-plugin', mockPlugin);
|
||||
const result = await service.reload('command');
|
||||
|
||||
expect(mockPlugin.onUnload).toHaveBeenCalledOnce();
|
||||
expect(mockPlugin.onLoad).toHaveBeenCalledOnce();
|
||||
expect(callOrder).toEqual(['onUnload', 'onLoad']);
|
||||
expect(result.message).toContain('test-plugin');
|
||||
});
|
||||
|
||||
it('reload() continues if one plugin throws during onUnload', async () => {
|
||||
const { service } = createService();
|
||||
|
||||
const badPlugin = {
|
||||
pluginName: 'bad-plugin',
|
||||
onLoad: vi.fn().mockResolvedValue(undefined),
|
||||
onUnload: vi.fn().mockRejectedValue(new Error('unload failed')),
|
||||
};
|
||||
|
||||
service.registerPlugin('bad-plugin', badPlugin);
|
||||
const result = await service.reload('command');
|
||||
|
||||
expect(result.message).toContain('bad-plugin');
|
||||
expect(result.message).toContain('unload failed');
|
||||
});
|
||||
|
||||
it('reload() skips non-MosaicPlugin objects', async () => {
|
||||
const { service } = createService();
|
||||
|
||||
const notAPlugin = { foo: 'bar' };
|
||||
service.registerPlugin('not-a-plugin', notAPlugin);
|
||||
|
||||
// Should not throw
|
||||
const result = await service.reload('command');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.message).not.toContain('not-a-plugin');
|
||||
});
|
||||
|
||||
it('reload() returns SystemReloadPayload with commands, skills, providers, message', async () => {
|
||||
const { service, registry } = createService();
|
||||
registry.getManifest.mockReturnValue({
|
||||
version: 1,
|
||||
commands: [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'test cmd',
|
||||
aliases: [],
|
||||
scope: 'core',
|
||||
execution: 'socket',
|
||||
available: true,
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
});
|
||||
|
||||
const result = await service.reload('rest');
|
||||
|
||||
expect(result).toHaveProperty('commands');
|
||||
expect(result).toHaveProperty('skills');
|
||||
expect(result).toHaveProperty('providers');
|
||||
expect(result).toHaveProperty('message');
|
||||
expect(result.commands).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('registerPlugin() logs plugin registration', () => {
|
||||
const { service } = createService();
|
||||
|
||||
// Should not throw and should register
|
||||
expect(() => service.registerPlugin('my-plugin', {})).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReloadService — /reload command sanitizes plugin errors', () => {
|
||||
it('generic per-plugin errors reach the chat surface while raw markers stay server-side only', async () => {
|
||||
const registry = {
|
||||
getManifest: vi.fn().mockReturnValue({
|
||||
version: 1,
|
||||
commands: [
|
||||
{ name: 'reload', aliases: [], scope: 'core', execution: 'socket', available: true },
|
||||
],
|
||||
skills: [],
|
||||
}),
|
||||
};
|
||||
const reloadService = new ReloadService(registry as never);
|
||||
|
||||
const RELOAD_LOAD_LEAK_MARKER = 'RELOAD_LOAD_LEAK_MARKER /srv/load-secret';
|
||||
const RELOAD_UNLOAD_LEAK_MARKER = 'RELOAD_UNLOAD_LEAK_MARKER /srv/unload-secret';
|
||||
|
||||
reloadService.registerPlugin('unload-fails', {
|
||||
pluginName: 'unload-fails',
|
||||
onLoad: vi.fn().mockResolvedValue(undefined),
|
||||
onUnload: vi.fn().mockRejectedValue(new Error(RELOAD_UNLOAD_LEAK_MARKER)),
|
||||
});
|
||||
reloadService.registerPlugin('load-fails', {
|
||||
pluginName: 'load-fails',
|
||||
onLoad: vi.fn().mockRejectedValue(new Error(RELOAD_LOAD_LEAK_MARKER)),
|
||||
onUnload: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
const errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
|
||||
const broadcastReload = vi.fn();
|
||||
const mockChatGateway = { broadcastReload };
|
||||
const mockAgentService = { getSession: vi.fn(), applyAgentConfig: vi.fn() };
|
||||
const mockSystemOverride = { set: vi.fn(), get: vi.fn(), clear: vi.fn() };
|
||||
const mockSessionGC = { sweepOrphans: vi.fn() };
|
||||
const mockBrain = { agents: { findByName: vi.fn(), findById: vi.fn(), create: vi.fn() } };
|
||||
|
||||
const mockMcpClient = {
|
||||
getServerStatuses: vi.fn(() => []),
|
||||
getToolDefinitions: vi.fn(() => []),
|
||||
reconnectServer: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const executor = new CommandExecutorService(
|
||||
registry as never,
|
||||
mockAgentService as never,
|
||||
mockSystemOverride as never,
|
||||
mockSessionGC as never,
|
||||
null,
|
||||
mockBrain as never,
|
||||
reloadService,
|
||||
mockChatGateway as never,
|
||||
mockMcpClient as never,
|
||||
{ authorize: vi.fn().mockResolvedValue({ allowed: true }) } as never,
|
||||
);
|
||||
|
||||
const payload: SlashCommandPayload = { command: 'reload', conversationId: 'conv-1' };
|
||||
const result = await executor.execute(payload, { userId: 'user-1', tenantId: 'user-1' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('unload-fails: unload failed (internal error)');
|
||||
expect(result.message).toContain('load-fails: load failed (internal error)');
|
||||
expect(result.message).not.toContain(RELOAD_UNLOAD_LEAK_MARKER);
|
||||
expect(result.message).not.toContain(RELOAD_LOAD_LEAK_MARKER);
|
||||
|
||||
expect(broadcastReload).toHaveBeenCalledOnce();
|
||||
const broadcastPayload = broadcastReload.mock.calls[0]?.[0] as SystemReloadPayload;
|
||||
expect(broadcastPayload.message).toContain('unload-fails: unload failed (internal error)');
|
||||
expect(broadcastPayload.message).toContain('load-fails: load failed (internal error)');
|
||||
expect(broadcastPayload.message).not.toContain(RELOAD_UNLOAD_LEAK_MARKER);
|
||||
expect(broadcastPayload.message).not.toContain(RELOAD_LOAD_LEAK_MARKER);
|
||||
|
||||
const loggedUnloadMarker = errorSpy.mock.calls.some((call) =>
|
||||
call.some((arg) => typeof arg === 'string' && arg.includes(RELOAD_UNLOAD_LEAK_MARKER)),
|
||||
);
|
||||
const loggedLoadMarker = errorSpy.mock.calls.some((call) =>
|
||||
call.some((arg) => typeof arg === 'string' && arg.includes(RELOAD_LOAD_LEAK_MARKER)),
|
||||
);
|
||||
expect(loggedUnloadMarker).toBe(true);
|
||||
expect(loggedLoadMarker).toBe(true);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user