UNPKG

@cyqlelabs/mcp-dual-cycle-reasoner

Version:

MCP server implementing dual-cycle metacognitive reasoning framework for autonomous agents

625 lines (491 loc) 20.9 kB
// @ts-nocheck import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { readFileSync } from 'fs'; import { join } from 'path'; // Mock chalk to avoid ES module issues in Jest jest.mock('chalk', () => ({ __esModule: true, default: { blue: (str: string) => str, green: (str: string) => str, yellow: (str: string) => str, red: (str: string) => str, gray: (str: string) => str, magenta: (str: string) => str, cyan: (str: string) => str, }, })); // Mock UUID to avoid potential issues jest.mock('uuid', () => ({ v4: () => 'test-uuid-1234', })); // Mock semantic analyzer to avoid initialization issues jest.mock('../src/semantic-analyzer', () => ({ semanticAnalyzer: { initialize: jest.fn(async () => {}), analyzeTextPair: jest.fn(async () => ({ similarity: 0.8, confidence: 0.9, sentiment: 0.1, })), assessActionOutcome: jest.fn(async () => ({ success_probability: 0.7, confidence: 0.8, sentiment: 0.2, })), assessBeliefContradiction: jest.fn(async () => ({ contradicts: true, confidence: 0.8, reasoning: 'Evidence contradicts belief', })), extractSemanticFeatures: jest.fn(async () => ({ intents: ['performing action', 'checking status'], sentiment: 'positive', confidence: 0.8, })), calculateSemanticSimilarity: jest.fn(async () => ({ similarity: 0.7, confidence: 0.8, reasoning: 'Semantic similarity analysis', })), getBatchEmbeddings: jest.fn( async (texts) => texts.map(() => Array(384).fill(0.5)) // Mock 384-dim embeddings ), computeSimilarityMatrix: jest.fn(async (texts) => { const n = texts.length; return Array(n) .fill() .map( (_, i) => Array(n) .fill() .map((_, j) => (i === j ? 1.0 : 0.7)) // Mock similarity matrix ); }), isReady: jest.fn(() => true), }, })); import { DualCycleEngine } from '../src/dual-cycle-engine'; import { SentinelConfig } from '../src/types'; describe('MCP Server Tools Tests', () => { let engine: DualCycleEngine; let config: Partial<SentinelConfig>; beforeEach(async () => { config = { progress_indicators: ['success', 'found', 'completed', 'navigated'], min_actions_for_detection: 3, alternating_threshold: 0.4, repetition_threshold: 0.3, progress_threshold_adjustment: 0.1, }; engine = new DualCycleEngine(config); // Initialize semantic analyzer for tests const { semanticAnalyzer } = await import('../src/semantic-analyzer'); await semanticAnalyzer.initialize(); }); afterEach(() => { engine.reset(); }); describe('start_monitoring tool', () => { it('should start monitoring with goal and initial beliefs', async () => { const goal = 'Test goal'; const initialBeliefs = ['belief1', 'belief2']; const startSpy = jest.spyOn(engine, 'startMonitoring'); await engine.startMonitoring(goal, initialBeliefs); expect(startSpy).toHaveBeenCalledWith(goal, initialBeliefs); const status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(true); expect(status.current_goal).toBe(goal); }); it('should handle empty initial beliefs', async () => { const goal = 'Test goal'; const startSpy = jest.spyOn(engine, 'startMonitoring'); await engine.startMonitoring(goal, []); expect(startSpy).toHaveBeenCalledWith(goal, []); const status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(true); }); it('should allow starting monitoring while already monitoring', async () => { await engine.startMonitoring('First goal', []); // Should not throw error - engine allows restarting monitoring await engine.startMonitoring('Second goal', []); const status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(true); expect(status.current_goal).toBe('Second goal'); }); }); describe('stop_monitoring tool', () => { it('should stop monitoring and return session summary', async () => { // Start monitoring first await engine.startMonitoring('Test goal', []); // Process some actions await engine.processTraceUpdate('test_action', 'test_context', 'Test goal'); const statusBeforeStop = engine.getMonitoringStatus(); expect(statusBeforeStop.is_monitoring).toBe(true); const stopSpy = jest.spyOn(engine, 'stopMonitoring'); engine.stopMonitoring(); expect(stopSpy).toHaveBeenCalled(); const statusAfterStop = engine.getMonitoringStatus(); expect(statusAfterStop.is_monitoring).toBe(false); }); it('should handle stopping when not monitoring', async () => { const stopSpy = jest.spyOn(engine, 'stopMonitoring'); // Should not throw error engine.stopMonitoring(); expect(stopSpy).toHaveBeenCalled(); const status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(false); }); }); describe('process_trace_update tool', () => { it('should process trace updates with all parameters', async () => { await engine.startMonitoring('Test goal', []); const processSpy = jest.spyOn(engine, 'processTraceUpdate'); const result = await engine.processTraceUpdate( 'test_action', 'test_context', 'Test goal', 10 ); expect(processSpy).toHaveBeenCalledWith('test_action', 'test_context', 'Test goal', 10); expect(result).toBeDefined(); expect(result.loop_detected).toBeDefined(); expect(result.intervention_required).toBeDefined(); }); it('should handle optional parameters', async () => { await engine.startMonitoring('Test goal', []); const result = await engine.processTraceUpdate('test_action', undefined, 'Test goal'); expect(result).toBeDefined(); expect(result.loop_detected).toBeDefined(); }); it('should handle window size parameter', async () => { await engine.startMonitoring('Test goal', []); const result = await engine.processTraceUpdate('test_action', 'test_context', 'Test goal', 5); expect(result).toBeDefined(); expect(result.loop_detected).toBeDefined(); }); it('should return intervention_required false when not monitoring', async () => { const result = await engine.processTraceUpdate('test_action', 'test_context', 'Test goal'); expect(result).toBeDefined(); expect(result.intervention_required).toBe(false); }); }); describe('detect_loop tool', () => { it('should detect loops with different methods', async () => { await engine.startMonitoring('Test goal', []); // Add some actions to create a trace await engine.processTraceUpdate('scroll_down', 'searching', 'Test goal'); await engine.processTraceUpdate('scroll_down', 'searching', 'Test goal'); await engine.processTraceUpdate('scroll_down', 'searching', 'Test goal'); const sentinel = (engine as any).sentinel; const enrichedTrace = engine.getEnrichedCurrentTrace(); // Test different detection methods const statisticalResult = await sentinel.detectLoop(enrichedTrace, 'statistical'); const patternResult = await sentinel.detectLoop(enrichedTrace, 'pattern'); const hybridResult = await sentinel.detectLoop(enrichedTrace, 'hybrid'); expect(statisticalResult).toBeDefined(); expect(patternResult).toBeDefined(); expect(hybridResult).toBeDefined(); expect(statisticalResult.detected).toBeDefined(); expect(patternResult.detected).toBeDefined(); expect(hybridResult.detected).toBeDefined(); }); it('should handle empty trace', async () => { await engine.startMonitoring('Test goal', []); const sentinel = (engine as any).sentinel; const enrichedTrace = engine.getEnrichedCurrentTrace(); const result = await sentinel.detectLoop(enrichedTrace, 'hybrid'); expect(result).toBeDefined(); expect(result.detected).toBe(false); }); it('should update context and goal when provided', async () => { await engine.startMonitoring('Original goal', []); // Add some actions await engine.processTraceUpdate('action1', 'context1', 'Original goal'); const sentinel = (engine as any).sentinel; const enrichedTrace = engine.getEnrichedCurrentTrace(); // Update with new context and goal const updatedTrace = { ...enrichedTrace, current_context: 'new_context', goal: 'Updated goal', }; const result = await sentinel.detectLoop(updatedTrace, 'hybrid'); expect(result).toBeDefined(); }); }); describe('store_experience tool', () => { it('should store experience cases', async () => { const experience = { problem_description: 'Test problem', solution: 'Test solution', outcome: true, }; const adjudicator = (engine as any).adjudicator; const storeSpy = jest.spyOn(adjudicator, 'storeExperience'); await adjudicator.storeExperience(experience); expect(storeSpy).toHaveBeenCalledWith(experience); }); it('should handle failed outcomes', async () => { const experience = { problem_description: 'Failed test problem', solution: 'Attempted solution', outcome: false, }; const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience(experience); // Verify it was stored const similarCases = await engine.getSimilarCases('Failed test problem', 5); expect(similarCases).toBeDefined(); }); it('should handle experience with context and metadata', async () => { const experience = { problem_description: 'Complex navigation problem', solution: 'Multi-step solution', outcome: true, context: 'web_navigation', difficulty_level: 'medium', }; const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience(experience); const similarCases = await engine.getSimilarCases('navigation problem', 5); expect(similarCases).toBeDefined(); }); }); describe('retrieve_similar_cases tool', () => { it('should retrieve similar cases with default parameters', async () => { // Store an experience first const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience({ problem_description: 'Navigation problem', solution: 'Use alternative method', outcome: true, }); const getSimilarSpy = jest.spyOn(engine, 'getSimilarCases'); const result = await engine.getSimilarCases('Navigation issue', 5); expect(getSimilarSpy).toHaveBeenCalledWith('Navigation issue', 5); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); }); it('should handle empty case base', async () => { // Reset to clear any stored cases engine.reset(); const result = await engine.getSimilarCases('Nonexistent problem', 5); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBe(0); }); it('should handle custom max_results parameter', async () => { // Store multiple experiences const adjudicator = (engine as any).adjudicator; for (let i = 0; i < 10; i++) { await adjudicator.storeExperience({ problem_description: `Problem ${i}`, solution: `Solution ${i}`, outcome: true, }); } const result = await engine.getSimilarCases('Problem', 3); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeLessThanOrEqual(3); }); it('should handle filters', async () => { // Store experiences with different outcomes const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience({ problem_description: 'Success case', solution: 'Working solution', outcome: true, }); await adjudicator.storeExperience({ problem_description: 'Failure case', solution: 'Failed solution', outcome: false, }); // Test with outcome filter const filters = { outcome_filter: true, context_filter: undefined, difficulty_filter: undefined, min_similarity: undefined, }; const result = await engine.getSimilarCases('case', 5, filters); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); }); }); describe('get_monitoring_status tool', () => { it('should return monitoring status when not monitoring', async () => { const status = engine.getMonitoringStatus(); expect(status).toBeDefined(); expect(status.is_monitoring).toBe(false); expect(status.current_goal).toBe(''); expect(status.trace_length).toBe(0); expect(status.intervention_count).toBe(0); }); it('should return monitoring status when actively monitoring', async () => { await engine.startMonitoring('Active goal', ['belief1']); await engine.processTraceUpdate('action1', 'context1', 'Active goal'); await engine.processTraceUpdate('action2', 'context2', 'Active goal'); const status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(true); expect(status.current_goal).toBe('Active goal'); expect(status.trace_length).toBeGreaterThan(0); }); it('should track intervention count', async () => { await engine.startMonitoring('Test goal', []); // Process actions that should trigger interventions const actions = ['scroll_down', 'scroll_down', 'scroll_down', 'scroll_down', 'scroll_down']; for (const action of actions) { const result = await engine.processTraceUpdate(action, 'searching', 'Test goal'); if (result.intervention_required) { break; } } const status = engine.getMonitoringStatus(); expect(status.intervention_count).toBeGreaterThan(0); }); }); describe('reset_engine tool', () => { it('should reset engine state completely', async () => { // Set up some state await engine.startMonitoring('Test goal', ['belief1']); await engine.processTraceUpdate('action1', 'context1', 'Test goal'); const statusBeforeReset = engine.getMonitoringStatus(); expect(statusBeforeReset.is_monitoring).toBe(true); expect(statusBeforeReset.trace_length).toBeGreaterThan(0); // Reset const resetSpy = jest.spyOn(engine, 'reset'); engine.reset(); expect(resetSpy).toHaveBeenCalled(); const statusAfterReset = engine.getMonitoringStatus(); expect(statusAfterReset.is_monitoring).toBe(false); expect(statusAfterReset.trace_length).toBe(0); expect(statusAfterReset.intervention_count).toBe(0); }); it('should preserve stored experiences after reset', async () => { // Store some experiences const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience({ problem_description: 'Test problem', solution: 'Test solution', outcome: true, }); // Verify it's stored let similarCases = await engine.getSimilarCases('Test problem', 5); expect(similarCases.length).toBeGreaterThan(0); // Reset engine.reset(); // Verify experiences are preserved (reset only clears monitoring state) similarCases = await engine.getSimilarCases('Test problem', 5); expect(similarCases.length).toBeGreaterThan(0); }); }); describe('configure_detection tool', () => { it('should update detection configuration', async () => { const newConfig = { progress_indicators: ['custom_indicator'], min_actions_for_detection: 3, alternating_threshold: 0.3, repetition_threshold: 0.2, progress_threshold_adjustment: 0.1, semantic_intents: ['custom_intent'], }; const sentinel = (engine as any).sentinel; const adjudicator = (engine as any).adjudicator; const updateConfigSpy = jest.spyOn(sentinel, 'updateConfig'); const updateSemanticSpy = jest.spyOn(adjudicator, 'updateSemanticIntents'); // Simulate configuration update sentinel.updateConfig(newConfig); adjudicator.updateSemanticIntents(newConfig.semantic_intents); expect(updateConfigSpy).toHaveBeenCalledWith(newConfig); expect(updateSemanticSpy).toHaveBeenCalledWith(newConfig.semantic_intents); }); it('should handle partial configuration updates', async () => { const partialConfig = { min_actions_for_detection: 7, alternating_threshold: 0.6, }; const sentinel = (engine as any).sentinel; const updateConfigSpy = jest.spyOn(sentinel, 'updateConfig'); sentinel.updateConfig(partialConfig); expect(updateConfigSpy).toHaveBeenCalledWith(partialConfig); }); it('should update semantic intents separately', async () => { const semanticIntents = ['intent1', 'intent2', 'intent3']; const adjudicator = (engine as any).adjudicator; const updateSemanticSpy = jest.spyOn(adjudicator, 'updateSemanticIntents'); adjudicator.updateSemanticIntents(semanticIntents); expect(updateSemanticSpy).toHaveBeenCalledWith(semanticIntents); }); it('should handle empty arrays in configuration', async () => { const configWithEmptyArrays = { progress_indicators: [], semantic_intents: [], }; const sentinel = (engine as any).sentinel; const adjudicator = (engine as any).adjudicator; const updateConfigSpy = jest.spyOn(sentinel, 'updateConfig'); const updateSemanticSpy = jest.spyOn(adjudicator, 'updateSemanticIntents'); sentinel.updateConfig(configWithEmptyArrays); adjudicator.updateSemanticIntents(configWithEmptyArrays.semantic_intents); expect(updateConfigSpy).toHaveBeenCalledWith(configWithEmptyArrays); expect(updateSemanticSpy).toHaveBeenCalledWith([]); }); }); describe('Tool Integration Tests', () => { it('should handle complete workflow from start to reset', async () => { // 1. Start monitoring await engine.startMonitoring('Complete workflow test', ['initial belief']); let status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(true); // 2. Process trace updates await engine.processTraceUpdate('action1', 'context1', 'Complete workflow test'); await engine.processTraceUpdate('action2', 'context2', 'Complete workflow test'); status = engine.getMonitoringStatus(); expect(status.trace_length).toBe(2); // 3. Detect loop const sentinel = (engine as any).sentinel; const enrichedTrace = engine.getEnrichedCurrentTrace(); const loopResult = await sentinel.detectLoop(enrichedTrace, 'hybrid'); expect(loopResult).toBeDefined(); // 4. Store experience const adjudicator = (engine as any).adjudicator; await adjudicator.storeExperience({ problem_description: 'Workflow test problem', solution: 'Workflow test solution', outcome: true, }); // 5. Retrieve similar cases const similarCases = await engine.getSimilarCases('Workflow test', 5); expect(similarCases).toBeDefined(); // 6. Configure detection const newConfig = { min_actions_for_detection: 5, alternating_threshold: 0.4, }; sentinel.updateConfig(newConfig); // 7. Stop monitoring engine.stopMonitoring(); status = engine.getMonitoringStatus(); expect(status.is_monitoring).toBe(false); // 8. Reset engine.reset(); status = engine.getMonitoringStatus(); expect(status.trace_length).toBe(0); }); it('should handle error conditions gracefully', async () => { // Test processing trace update without starting monitoring - returns intervention_required false const result = await engine.processTraceUpdate('action', 'context', 'goal'); expect(result.intervention_required).toBe(false); // Test getting enriched trace without monitoring - returns default values const enrichedTrace = engine.getEnrichedCurrentTrace(); expect(enrichedTrace).toBeDefined(); expect(enrichedTrace.last_action).toBe(''); expect(enrichedTrace.current_context).toBeUndefined(); expect(enrichedTrace.goal).toBe(''); expect(enrichedTrace.recent_actions).toEqual([]); }); }); });