Files
SuperCharged-Claude-Code-Up…/dexto/packages/core/src/agent/DextoAgent.lifecycle.test.ts
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 00:27:56 +04:00

413 lines
15 KiB
TypeScript

import { describe, test, expect, vi, beforeEach } from 'vitest';
import { DextoAgent } from './DextoAgent.js';
import type { AgentConfig, ValidatedAgentConfig } from './schemas.js';
import { AgentConfigSchema } from './schemas.js';
import type { AgentServices } from '../utils/service-initializer.js';
import { DextoRuntimeError } from '../errors/DextoRuntimeError.js';
import { ErrorScope, ErrorType } from '../errors/types.js';
import { AgentErrorCode } from './error-codes.js';
// Mock the createAgentServices function
vi.mock('../utils/service-initializer.js', () => ({
createAgentServices: vi.fn(),
}));
import { createAgentServices } from '../utils/service-initializer.js';
const mockCreateAgentServices = vi.mocked(createAgentServices);
describe('DextoAgent Lifecycle Management', () => {
let mockConfig: AgentConfig;
let mockValidatedConfig: ValidatedAgentConfig;
let mockServices: AgentServices;
beforeEach(() => {
vi.resetAllMocks();
mockConfig = {
systemPrompt: 'You are a helpful assistant',
llm: {
provider: 'openai',
model: 'gpt-5',
apiKey: 'test-key',
maxIterations: 50,
maxInputTokens: 128000,
},
mcpServers: {},
sessions: {
maxSessions: 10,
sessionTTL: 3600,
},
toolConfirmation: {
mode: 'auto-approve',
timeout: 120000,
},
elicitation: {
enabled: false,
timeout: 120000,
},
};
// Create the validated config that DextoAgent actually uses
mockValidatedConfig = AgentConfigSchema.parse(mockConfig);
mockServices = {
mcpManager: {
disconnectAll: vi.fn(),
initializeFromConfig: vi.fn().mockResolvedValue(undefined),
} as any,
toolManager: {
setAgent: vi.fn(),
setPromptManager: vi.fn(),
initialize: vi.fn().mockResolvedValue(undefined),
} as any,
systemPromptManager: {} as any,
agentEventBus: {
on: vi.fn(),
emit: vi.fn(),
} as any,
stateManager: {
getRuntimeConfig: vi.fn().mockReturnValue({
llm: mockValidatedConfig.llm,
mcpServers: {},
storage: {
cache: { type: 'in-memory' },
database: { type: 'in-memory' },
},
sessions: {
maxSessions: 10,
sessionTTL: 3600,
},
}),
getLLMConfig: vi.fn().mockReturnValue(mockValidatedConfig.llm),
} as any,
sessionManager: {
cleanup: vi.fn(),
init: vi.fn().mockResolvedValue(undefined),
createSession: vi.fn().mockResolvedValue({ id: 'test-session' }),
} as any,
searchService: {} as any,
storageManager: {
disconnect: vi.fn(),
getDatabase: vi.fn().mockReturnValue({}),
getCache: vi.fn().mockReturnValue({}),
getBlobStore: vi.fn().mockReturnValue({}),
} as any,
resourceManager: {} as any,
approvalManager: {
requestToolConfirmation: vi.fn(),
requestElicitation: vi.fn(),
cancelApproval: vi.fn(),
cancelAllApprovals: vi.fn(),
hasHandler: vi.fn().mockReturnValue(false),
} as any,
memoryManager: {} as any,
pluginManager: {
cleanup: vi.fn(),
} as any,
};
mockCreateAgentServices.mockResolvedValue(mockServices);
// Set up default behaviors for mock functions that will be overridden in tests
(mockServices.sessionManager.cleanup as any).mockResolvedValue(undefined);
(mockServices.mcpManager.disconnectAll as any).mockResolvedValue(undefined);
(mockServices.storageManager!.disconnect as any).mockResolvedValue(undefined);
});
describe('Constructor Patterns', () => {
test('should create agent with config (new pattern)', () => {
const agent = new DextoAgent(mockConfig);
expect(agent.isStarted()).toBe(false);
expect(agent.isStopped()).toBe(false);
});
});
describe('start() Method', () => {
test('should start successfully with valid config', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
expect(agent.isStarted()).toBe(true);
expect(agent.isStopped()).toBe(false);
expect(mockCreateAgentServices).toHaveBeenCalledWith(
mockValidatedConfig,
undefined,
expect.anything(), // logger instance
expect.anything() // eventBus instance
);
});
test('should start with per-server connection modes in config', async () => {
const configWithServerModes = {
...mockConfig,
mcpServers: {
filesystem: {
type: 'stdio' as const,
command: 'npx',
args: ['@modelcontextprotocol/server-filesystem', '.'],
env: {},
timeout: 30000,
connectionMode: 'strict' as const,
},
},
};
const agent = new DextoAgent(configWithServerModes);
await agent.start();
const validatedConfigWithServerModes = AgentConfigSchema.parse(configWithServerModes);
expect(mockCreateAgentServices).toHaveBeenCalledWith(
validatedConfigWithServerModes,
undefined,
expect.anything(), // logger instance
expect.anything() // eventBus instance
);
});
test('should throw error when starting twice', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
await expect(agent.start()).rejects.toThrow(
expect.objectContaining({
code: AgentErrorCode.ALREADY_STARTED,
scope: ErrorScope.AGENT,
type: ErrorType.USER,
})
);
});
test('should handle start failure gracefully', async () => {
const agent = new DextoAgent(mockConfig);
mockCreateAgentServices.mockRejectedValue(new Error('Service initialization failed'));
await expect(agent.start()).rejects.toThrow('Service initialization failed');
expect(agent.isStarted()).toBe(false);
});
});
describe('stop() Method', () => {
test('should stop successfully after start', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
await agent.stop();
expect(agent.isStarted()).toBe(false);
expect(agent.isStopped()).toBe(true);
expect(mockServices.sessionManager.cleanup).toHaveBeenCalled();
expect(mockServices.mcpManager.disconnectAll).toHaveBeenCalled();
expect(mockServices.storageManager!.disconnect).toHaveBeenCalled();
});
test('should throw error when stopping before start', async () => {
const agent = new DextoAgent(mockConfig);
await expect(agent.stop()).rejects.toThrow(
expect.objectContaining({
code: AgentErrorCode.NOT_STARTED,
scope: ErrorScope.AGENT,
type: ErrorType.USER,
})
);
});
test('should warn when stopping twice but not throw', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
await agent.stop();
// Second stop should not throw but should warn
await expect(agent.stop()).resolves.toBeUndefined();
});
test('should handle partial cleanup failures gracefully', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
// Make session cleanup fail
(mockServices.sessionManager.cleanup as any).mockRejectedValue(
new Error('Session cleanup failed')
);
// Should not throw, but should still mark as stopped
await expect(agent.stop()).resolves.toBeUndefined();
expect(agent.isStopped()).toBe(true);
// Should still try to clean other services
expect(mockServices.mcpManager.disconnectAll).toHaveBeenCalled();
expect(mockServices.storageManager!.disconnect).toHaveBeenCalled();
});
});
describe('Method Access Control', () => {
const testMethods = [
{ name: 'run', args: ['test message'] },
{ name: 'createSession', args: [] },
{ name: 'getSession', args: ['session-id'] },
{ name: 'listSessions', args: [] },
{ name: 'deleteSession', args: ['session-id'] },
{ name: 'resetConversation', args: [] },
{ name: 'getCurrentLLMConfig', args: [] },
{ name: 'switchLLM', args: [{ model: 'gpt-5' }] },
{ name: 'addMcpServer', args: ['test', { type: 'stdio', command: 'test' }] },
{ name: 'getAllMcpTools', args: [] },
];
test.each(testMethods)('$name should throw before start()', async ({ name, args }) => {
const agent = new DextoAgent(mockConfig);
let thrownError: DextoRuntimeError | undefined;
try {
const method = agent[name as keyof DextoAgent] as Function;
await method.apply(agent, args);
} catch (error) {
thrownError = error as DextoRuntimeError;
}
expect(thrownError).toBeDefined();
expect(thrownError).toMatchObject({
code: AgentErrorCode.NOT_STARTED,
scope: ErrorScope.AGENT,
type: ErrorType.USER,
});
});
test.each(testMethods)('$name should throw after stop()', async ({ name, args }) => {
const agent = new DextoAgent(mockConfig);
await agent.start();
await agent.stop();
let thrownError: DextoRuntimeError | undefined;
try {
const method = agent[name as keyof DextoAgent] as Function;
await method.apply(agent, args);
} catch (error) {
thrownError = error as DextoRuntimeError;
}
expect(thrownError).toBeDefined();
expect(thrownError).toMatchObject({
code: AgentErrorCode.STOPPED,
scope: ErrorScope.AGENT,
type: ErrorType.USER,
});
});
test('isStarted and isStopped should work without start() (read-only)', () => {
const agent = new DextoAgent(mockConfig);
expect(() => agent.isStarted()).not.toThrow();
expect(() => agent.isStopped()).not.toThrow();
});
});
describe('Session Auto-Approve Tools Cleanup (Memory Leak Fix)', () => {
test('endSession should call clearSessionAutoApproveTools', async () => {
const agent = new DextoAgent(mockConfig);
// Add clearSessionAutoApproveTools mock to toolManager
mockServices.toolManager.clearSessionAutoApproveTools = vi.fn();
mockServices.sessionManager.endSession = vi.fn().mockResolvedValue(undefined);
await agent.start();
await agent.endSession('test-session-123');
expect(mockServices.toolManager.clearSessionAutoApproveTools).toHaveBeenCalledWith(
'test-session-123'
);
expect(mockServices.sessionManager.endSession).toHaveBeenCalledWith('test-session-123');
});
test('deleteSession should call clearSessionAutoApproveTools', async () => {
const agent = new DextoAgent(mockConfig);
// Add clearSessionAutoApproveTools mock to toolManager
mockServices.toolManager.clearSessionAutoApproveTools = vi.fn();
mockServices.sessionManager.deleteSession = vi.fn().mockResolvedValue(undefined);
await agent.start();
await agent.deleteSession('test-session-456');
expect(mockServices.toolManager.clearSessionAutoApproveTools).toHaveBeenCalledWith(
'test-session-456'
);
expect(mockServices.sessionManager.deleteSession).toHaveBeenCalledWith(
'test-session-456'
);
});
test('clearSessionAutoApproveTools should be called before session cleanup', async () => {
const agent = new DextoAgent(mockConfig);
const callOrder: string[] = [];
mockServices.toolManager.clearSessionAutoApproveTools = vi.fn(() => {
callOrder.push('clearSessionAutoApproveTools');
});
mockServices.sessionManager.endSession = vi.fn().mockImplementation(() => {
callOrder.push('endSession');
return Promise.resolve();
});
await agent.start();
await agent.endSession('test-session');
expect(callOrder).toEqual(['clearSessionAutoApproveTools', 'endSession']);
});
});
describe('Integration Tests', () => {
test('should handle complete lifecycle without errors', async () => {
const agent = new DextoAgent(mockConfig);
// Initial state
expect(agent.isStarted()).toBe(false);
expect(agent.isStopped()).toBe(false);
// Start
await agent.start();
expect(agent.isStarted()).toBe(true);
expect(agent.isStopped()).toBe(false);
// Use agent (mock a successful operation)
expect(agent.getCurrentLLMConfig()).toBeDefined();
// Stop
await agent.stop();
expect(agent.isStarted()).toBe(false);
expect(agent.isStopped()).toBe(true);
});
test('should handle resource cleanup in correct order', async () => {
const agent = new DextoAgent(mockConfig);
await agent.start();
const cleanupOrder: string[] = [];
(mockServices.sessionManager.cleanup as any).mockImplementation(() => {
cleanupOrder.push('sessions');
return Promise.resolve();
});
(mockServices.mcpManager.disconnectAll as any).mockImplementation(() => {
cleanupOrder.push('clients');
return Promise.resolve();
});
(mockServices.storageManager!.disconnect as any).mockImplementation(() => {
cleanupOrder.push('storage');
return Promise.resolve();
});
await agent.stop();
expect(cleanupOrder).toEqual(['sessions', 'clients', 'storage']);
});
});
});