test(intelligence): update tests to use intelligenceClient
- Rewrite context-compactor.test.ts to use intelligenceClient - Rewrite heartbeat-reflection.test.ts to use intelligenceClient - Rewrite swarm-skills.test.ts to use intelligenceClient - Update CLAUDE.md architecture section for unified intelligence layer All tests now mock Tauri backend calls for unit testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,15 @@
|
||||
/**
|
||||
* Tests for Heartbeat Engine + Reflection Engine (Phase 3)
|
||||
* Tests for Heartbeat + Reflection (via intelligenceClient)
|
||||
*
|
||||
* These tests mock the Tauri backend calls for unit testing.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
HeartbeatEngine,
|
||||
resetHeartbeatEngines,
|
||||
type HeartbeatAlert,
|
||||
} from '../../desktop/src/lib/heartbeat-engine';
|
||||
import {
|
||||
ReflectionEngine,
|
||||
resetReflectionEngine,
|
||||
} from '../../desktop/src/lib/reflection-engine';
|
||||
import { MemoryManager, resetMemoryManager } from '../../desktop/src/lib/agent-memory';
|
||||
import { resetAgentIdentityManager } from '../../desktop/src/lib/agent-identity';
|
||||
import { resetMemoryExtractor } from '../../desktop/src/lib/memory-extractor';
|
||||
intelligenceClient,
|
||||
type HeartbeatConfig,
|
||||
type HeartbeatResult,
|
||||
} from '../../desktop/src/lib/intelligence-client';
|
||||
|
||||
// === Mock localStorage ===
|
||||
|
||||
@@ -30,394 +25,152 @@ const localStorageMock = (() => {
|
||||
|
||||
vi.stubGlobal('localStorage', localStorageMock);
|
||||
|
||||
// === Mock Tauri invoke ===
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: vi.fn(async (cmd: string, _args?: unknown) => {
|
||||
// Mock responses for heartbeat commands
|
||||
if (cmd === 'heartbeat_init') {
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'heartbeat_start') {
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'heartbeat_stop') {
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'heartbeat_tick') {
|
||||
return {
|
||||
status: 'ok',
|
||||
alerts: [],
|
||||
checked_items: 3,
|
||||
timestamp: new Date().toISOString(),
|
||||
} as HeartbeatResult;
|
||||
}
|
||||
if (cmd === 'heartbeat_get_status') {
|
||||
return {
|
||||
running: false,
|
||||
last_tick: null,
|
||||
next_tick: null,
|
||||
config: _args?.config || null,
|
||||
};
|
||||
}
|
||||
// Reflection commands
|
||||
if (cmd === 'reflection_reflect') {
|
||||
return {
|
||||
patterns: [],
|
||||
improvements: [],
|
||||
identity_proposals: [],
|
||||
new_memories: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
if (cmd === 'reflection_get_history') {
|
||||
return [];
|
||||
}
|
||||
if (cmd === 'reflection_get_state') {
|
||||
return {
|
||||
conversations_since_reflection: 0,
|
||||
last_reflection_time: null,
|
||||
last_reflection_agent_id: null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
}));
|
||||
|
||||
// =============================================
|
||||
// HeartbeatEngine Tests
|
||||
// Heartbeat Tests (via intelligenceClient)
|
||||
// =============================================
|
||||
|
||||
describe('HeartbeatEngine', () => {
|
||||
let engine: HeartbeatEngine;
|
||||
describe('Heartbeat (via intelligenceClient)', () => {
|
||||
const defaultConfig: HeartbeatConfig = {
|
||||
enabled: true,
|
||||
interval_minutes: 30,
|
||||
quiet_hours_start: null,
|
||||
quiet_hours_end: null,
|
||||
notify_channel: 'ui',
|
||||
proactivity_level: 'standard',
|
||||
max_alerts_per_tick: 5,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
resetHeartbeatEngines();
|
||||
resetMemoryManager();
|
||||
resetAgentIdentityManager();
|
||||
// Disable quiet hours to avoid test-time sensitivity
|
||||
engine = new HeartbeatEngine('agent-1', { quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
engine.stop();
|
||||
});
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('starts and stops cleanly', () => {
|
||||
const eng = new HeartbeatEngine('test', { enabled: true, intervalMinutes: 1 });
|
||||
eng.start();
|
||||
expect(eng.isRunning()).toBe(true);
|
||||
eng.stop();
|
||||
expect(eng.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not start when disabled', () => {
|
||||
const eng = new HeartbeatEngine('test', { enabled: false });
|
||||
eng.start();
|
||||
expect(eng.isRunning()).toBe(false);
|
||||
describe('init', () => {
|
||||
it('initializes heartbeat with config', async () => {
|
||||
await expect(
|
||||
intelligenceClient.heartbeat.init('test-agent', defaultConfig)
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tick', () => {
|
||||
it('returns ok status when no alerts', async () => {
|
||||
const result = await engine.tick();
|
||||
expect(result.status).toBe('ok');
|
||||
expect(result.checkedItems).toBeGreaterThan(0);
|
||||
expect(result.timestamp).toBeTruthy();
|
||||
it('returns heartbeat result', async () => {
|
||||
const result = await intelligenceClient.heartbeat.tick('test-agent');
|
||||
|
||||
expect(result).toHaveProperty('status');
|
||||
expect(result).toHaveProperty('alerts');
|
||||
expect(result).toHaveProperty('checked_items');
|
||||
expect(result).toHaveProperty('timestamp');
|
||||
});
|
||||
|
||||
it('detects pending tasks', async () => {
|
||||
const mgr = new MemoryManager();
|
||||
// Create high-importance task memories
|
||||
await mgr.save({ agentId: 'agent-1', content: '完成API集成', type: 'task', importance: 8, source: 'auto', tags: [] });
|
||||
await mgr.save({ agentId: 'agent-1', content: '修复登录bug', type: 'task', importance: 7, source: 'auto', tags: [] });
|
||||
|
||||
const result = await engine.tick();
|
||||
// With 'light' proactivity, only high urgency alerts pass through
|
||||
// The task check produces medium/high urgency
|
||||
const taskAlerts = result.alerts.filter(a => a.source === 'pending-tasks');
|
||||
// May or may not produce alert depending on proactivity filter
|
||||
expect(result.checkedItems).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('detects memory health issues', async () => {
|
||||
const mgr = new MemoryManager();
|
||||
// Create many memories to trigger health alert
|
||||
for (let i = 0; i < 510; i++) {
|
||||
await mgr.save({
|
||||
agentId: 'agent-1',
|
||||
content: `memory entry ${i} with unique content ${Math.random()}`,
|
||||
type: 'fact',
|
||||
importance: 3,
|
||||
source: 'auto',
|
||||
tags: [`batch${i}`],
|
||||
});
|
||||
}
|
||||
|
||||
// Use autonomous proactivity to see all alerts
|
||||
const eng = new HeartbeatEngine('agent-1', { proactivityLevel: 'autonomous', quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
const result = await eng.tick();
|
||||
const healthAlerts = result.alerts.filter(a => a.source === 'memory-health');
|
||||
expect(healthAlerts.length).toBe(1);
|
||||
expect(healthAlerts[0].content).toMatch(/\d{3,}/);
|
||||
});
|
||||
|
||||
it('stores tick results in history', async () => {
|
||||
await engine.tick();
|
||||
await engine.tick();
|
||||
|
||||
const history = engine.getHistory();
|
||||
expect(history.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quiet hours', () => {
|
||||
it('returns ok with 0 checks during quiet hours', async () => {
|
||||
// Set quiet hours to cover current time
|
||||
const now = new Date();
|
||||
const startHour = now.getHours();
|
||||
const endHour = (startHour + 2) % 24;
|
||||
const eng = new HeartbeatEngine('test', {
|
||||
quietHoursStart: `${String(startHour).padStart(2, '0')}:00`,
|
||||
quietHoursEnd: `${String(endHour).padStart(2, '0')}:00`,
|
||||
});
|
||||
|
||||
const result = await eng.tick();
|
||||
expect(result.checkedItems).toBe(0);
|
||||
it('returns ok status when no issues', async () => {
|
||||
const result = await intelligenceClient.heartbeat.tick('test-agent');
|
||||
expect(result.status).toBe('ok');
|
||||
});
|
||||
|
||||
it('isQuietHours handles cross-midnight range', () => {
|
||||
const eng = new HeartbeatEngine('test', {
|
||||
quietHoursStart: '22:00',
|
||||
quietHoursEnd: '08:00',
|
||||
});
|
||||
|
||||
// The method checks against current time, so we just verify it doesn't throw
|
||||
const result = eng.isQuietHours();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom checks', () => {
|
||||
it('runs registered custom checks', async () => {
|
||||
const eng = new HeartbeatEngine('test', { proactivityLevel: 'autonomous', quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
eng.registerCheck(async () => ({
|
||||
title: 'Custom Alert',
|
||||
content: 'Custom check triggered',
|
||||
urgency: 'medium' as const,
|
||||
source: 'custom',
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
const result = await eng.tick();
|
||||
const custom = result.alerts.filter(a => a.source === 'custom');
|
||||
expect(custom.length).toBe(1);
|
||||
expect(custom[0].title).toBe('Custom Alert');
|
||||
});
|
||||
});
|
||||
|
||||
describe('proactivity filtering', () => {
|
||||
it('silent mode suppresses all alerts', async () => {
|
||||
const eng = new HeartbeatEngine('test', { proactivityLevel: 'silent', quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
eng.registerCheck(async () => ({
|
||||
title: 'Test',
|
||||
content: 'Test',
|
||||
urgency: 'high' as const,
|
||||
source: 'test',
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
const result = await eng.tick();
|
||||
expect(result.alerts).toHaveLength(0);
|
||||
describe('start/stop', () => {
|
||||
it('starts heartbeat', async () => {
|
||||
await expect(
|
||||
intelligenceClient.heartbeat.start('test-agent')
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('light mode only shows high urgency', async () => {
|
||||
const eng = new HeartbeatEngine('test', { proactivityLevel: 'light', quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
eng.registerCheck(async () => ({
|
||||
title: 'Low',
|
||||
content: 'Low urgency',
|
||||
urgency: 'low' as const,
|
||||
source: 'test-low',
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
eng.registerCheck(async () => ({
|
||||
title: 'High',
|
||||
content: 'High urgency',
|
||||
urgency: 'high' as const,
|
||||
source: 'test-high',
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
const result = await eng.tick();
|
||||
expect(result.alerts.every(a => a.urgency === 'high')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config', () => {
|
||||
it('returns current config', () => {
|
||||
const config = engine.getConfig();
|
||||
expect(config.intervalMinutes).toBe(30);
|
||||
expect(config.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('updates config', () => {
|
||||
engine.updateConfig({ intervalMinutes: 15 });
|
||||
expect(engine.getConfig().intervalMinutes).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alert callback', () => {
|
||||
it('calls onAlert when alerts are produced', async () => {
|
||||
const alerts: HeartbeatAlert[][] = [];
|
||||
const eng = new HeartbeatEngine('test', { enabled: true, proactivityLevel: 'autonomous', quietHoursStart: undefined, quietHoursEnd: undefined });
|
||||
eng.registerCheck(async () => ({
|
||||
title: 'Alert',
|
||||
content: 'Test alert',
|
||||
urgency: 'high' as const,
|
||||
source: 'test',
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
eng.start((a) => alerts.push(a));
|
||||
// Manually trigger tick instead of waiting for interval
|
||||
await eng.tick();
|
||||
eng.stop();
|
||||
|
||||
// The tick() call should have triggered onAlert
|
||||
// Note: the start() interval won't fire in test, but manual tick() does call onAlert
|
||||
it('stops heartbeat', async () => {
|
||||
await expect(
|
||||
intelligenceClient.heartbeat.stop('test-agent')
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================
|
||||
// ReflectionEngine Tests
|
||||
// Reflection Tests (via intelligenceClient)
|
||||
// =============================================
|
||||
|
||||
describe('ReflectionEngine', () => {
|
||||
let engine: ReflectionEngine;
|
||||
|
||||
describe('Reflection (via intelligenceClient)', () => {
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
resetReflectionEngine();
|
||||
resetMemoryManager();
|
||||
resetAgentIdentityManager();
|
||||
resetMemoryExtractor();
|
||||
engine = new ReflectionEngine();
|
||||
});
|
||||
|
||||
describe('trigger management', () => {
|
||||
it('should not reflect initially', () => {
|
||||
expect(engine.shouldReflect()).toBe(false);
|
||||
});
|
||||
|
||||
it('triggers after N conversations', () => {
|
||||
const eng = new ReflectionEngine({ triggerAfterConversations: 3 });
|
||||
eng.recordConversation();
|
||||
eng.recordConversation();
|
||||
expect(eng.shouldReflect()).toBe(false);
|
||||
eng.recordConversation();
|
||||
expect(eng.shouldReflect()).toBe(true);
|
||||
});
|
||||
|
||||
it('resets counter after reflection', async () => {
|
||||
const eng = new ReflectionEngine({ triggerAfterConversations: 2 });
|
||||
eng.recordConversation();
|
||||
eng.recordConversation();
|
||||
expect(eng.shouldReflect()).toBe(true);
|
||||
|
||||
await eng.reflect('agent-1');
|
||||
expect(eng.shouldReflect()).toBe(false);
|
||||
expect(eng.getState().conversationsSinceReflection).toBe(0);
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('reflect', () => {
|
||||
it('returns result with patterns and improvements', async () => {
|
||||
const result = await engine.reflect('agent-1');
|
||||
it('returns reflection result', async () => {
|
||||
const result = await intelligenceClient.reflection.reflect('test-agent', []);
|
||||
|
||||
expect(result.timestamp).toBeTruthy();
|
||||
expect(Array.isArray(result.patterns)).toBe(true);
|
||||
expect(Array.isArray(result.improvements)).toBe(true);
|
||||
expect(typeof result.newMemories).toBe('number');
|
||||
});
|
||||
|
||||
it('detects task accumulation pattern', async () => {
|
||||
const mgr = new MemoryManager();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await mgr.save({
|
||||
agentId: 'agent-1',
|
||||
content: `Task ${i}: do something ${Math.random()}`,
|
||||
type: 'task',
|
||||
importance: 6,
|
||||
source: 'auto',
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await engine.reflect('agent-1');
|
||||
const taskPattern = result.patterns.find(p => p.observation.includes('待办任务'));
|
||||
expect(taskPattern).toBeTruthy();
|
||||
expect(taskPattern!.sentiment).toBe('negative');
|
||||
});
|
||||
|
||||
it('detects strong preference accumulation as positive', async () => {
|
||||
const mgr = new MemoryManager();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await mgr.save({
|
||||
agentId: 'agent-1',
|
||||
content: `Preference ${i}: likes ${Math.random()}`,
|
||||
type: 'preference',
|
||||
importance: 5,
|
||||
source: 'auto',
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await engine.reflect('agent-1');
|
||||
const prefPattern = result.patterns.find(p => p.observation.includes('用户偏好'));
|
||||
expect(prefPattern).toBeTruthy();
|
||||
expect(prefPattern!.sentiment).toBe('positive');
|
||||
});
|
||||
|
||||
it('generates improvement suggestions for low preference count', async () => {
|
||||
// No preferences saved → should suggest enrichment
|
||||
const result = await engine.reflect('agent-1');
|
||||
const userImprovement = result.improvements.find(i => i.area === '用户理解');
|
||||
expect(userImprovement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('saves reflection memories', async () => {
|
||||
const mgr = new MemoryManager();
|
||||
// Create enough data for patterns to be detected
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await mgr.save({
|
||||
agentId: 'agent-1',
|
||||
content: `Task ${i}: important work item ${Math.random()}`,
|
||||
type: 'task',
|
||||
importance: 7,
|
||||
source: 'auto',
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await engine.reflect('agent-1');
|
||||
expect(result.newMemories).toBeGreaterThan(0);
|
||||
|
||||
// Verify reflection memories were saved (reload from localStorage since reflect uses singleton)
|
||||
const mgr2 = new MemoryManager();
|
||||
const allMemories = await mgr2.getAll('agent-1');
|
||||
const reflectionMemories = allMemories.filter(m => m.tags.includes('reflection'));
|
||||
expect(reflectionMemories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('stores result in history', async () => {
|
||||
await engine.reflect('agent-1');
|
||||
await engine.reflect('agent-1');
|
||||
|
||||
const history = engine.getHistory();
|
||||
expect(history.length).toBe(2);
|
||||
expect(result).toHaveProperty('patterns');
|
||||
expect(result).toHaveProperty('improvements');
|
||||
expect(result).toHaveProperty('identity_proposals');
|
||||
expect(result).toHaveProperty('new_memories');
|
||||
expect(result).toHaveProperty('timestamp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('identity proposals', () => {
|
||||
it('proposes changes when allowSoulModification is true', async () => {
|
||||
const eng = new ReflectionEngine({ allowSoulModification: true });
|
||||
const mgr = new MemoryManager();
|
||||
|
||||
// Create multiple negative patterns
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await mgr.save({
|
||||
agentId: 'agent-1',
|
||||
content: `Overdue task ${i}: ${Math.random()}`,
|
||||
type: 'task',
|
||||
importance: 7,
|
||||
source: 'auto',
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
const result = await eng.reflect('agent-1');
|
||||
// May or may not produce identity proposals depending on pattern analysis
|
||||
expect(Array.isArray(result.identityProposals)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not propose changes when allowSoulModification is false', async () => {
|
||||
const eng = new ReflectionEngine({ allowSoulModification: false });
|
||||
const result = await eng.reflect('agent-1');
|
||||
expect(result.identityProposals).toHaveLength(0);
|
||||
describe('getHistory', () => {
|
||||
it('returns reflection history', async () => {
|
||||
const history = await intelligenceClient.reflection.getHistory();
|
||||
expect(Array.isArray(history)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config', () => {
|
||||
it('returns current config', () => {
|
||||
const config = engine.getConfig();
|
||||
expect(config.triggerAfterConversations).toBe(5);
|
||||
expect(config.requireApproval).toBe(true);
|
||||
});
|
||||
|
||||
it('updates config', () => {
|
||||
engine.updateConfig({ triggerAfterConversations: 10 });
|
||||
expect(engine.getConfig().triggerAfterConversations).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistence', () => {
|
||||
it('persists state across instances', () => {
|
||||
engine.recordConversation();
|
||||
engine.recordConversation();
|
||||
|
||||
resetReflectionEngine();
|
||||
const eng2 = new ReflectionEngine();
|
||||
expect(eng2.getState().conversationsSinceReflection).toBe(2);
|
||||
describe('getState', () => {
|
||||
it('returns reflection state', async () => {
|
||||
const state = await intelligenceClient.reflection.getState();
|
||||
expect(state).toHaveProperty('conversations_since_reflection');
|
||||
expect(state).toHaveProperty('last_reflection_time');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user