chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Tests for MemoryManager.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock pi-ai complete function
|
||||
vi.mock('@mariozechner/pi-ai', () => ({
|
||||
complete: vi.fn().mockResolvedValue({
|
||||
content: [{ type: 'text', text: '{"summary": "Steps 1-5: navigated and clicked"}' }],
|
||||
}),
|
||||
getModel: vi.fn().mockReturnValue({ provider: 'mock', model: 'mock-model' }),
|
||||
}));
|
||||
|
||||
import { MemoryManager } from '../../src/agent/memoryManager.js';
|
||||
import { getModel } from '@mariozechner/pi-ai';
|
||||
|
||||
describe('MemoryManager', () => {
|
||||
let mm: MemoryManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const model = getModel('anthropic' as any, 'claude-sonnet-4-20250514' as any);
|
||||
mm = new MemoryManager(model, 'test-key');
|
||||
});
|
||||
|
||||
describe('addPendingStep + compose', () => {
|
||||
it('should add pending step and include it in compose', () => {
|
||||
mm.addPendingStep(1, 'Click button');
|
||||
const text = mm.compose();
|
||||
expect(text).toContain('[Current step]');
|
||||
expect(text).toContain('Step 1');
|
||||
expect(text).toContain('Click button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalizeStep', () => {
|
||||
it('should move pending to recent on finalize', async () => {
|
||||
mm.addPendingStep(1, 'Click button');
|
||||
await mm.finalizeStep(1, 'success');
|
||||
|
||||
const text = mm.compose();
|
||||
expect(text).toContain('[Recent steps]');
|
||||
expect(text).toContain('Eval: success');
|
||||
expect(text).not.toContain('[Current step]');
|
||||
});
|
||||
|
||||
it('should record failed evaluation', async () => {
|
||||
mm.addPendingStep(1, 'Click button');
|
||||
await mm.finalizeStep(1, 'failed');
|
||||
|
||||
const text = mm.compose();
|
||||
expect(text).toContain('Eval: failed');
|
||||
});
|
||||
|
||||
it('should accumulate multiple steps in recent', async () => {
|
||||
mm.addPendingStep(1, 'Step one');
|
||||
await mm.finalizeStep(1, 'success');
|
||||
mm.addPendingStep(2, 'Step two');
|
||||
await mm.finalizeStep(2, 'success');
|
||||
|
||||
const text = mm.compose();
|
||||
expect(text).toContain('Step 1');
|
||||
expect(text).toContain('Step 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compression', () => {
|
||||
it('should trigger compression when recent exceeds budget', async () => {
|
||||
const { complete } = await import('@mariozechner/pi-ai');
|
||||
|
||||
// Add many steps to exceed the 500 char budget
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
mm.addPendingStep(i, `Goal for step ${i}: perform a complex multi-word action description`);
|
||||
await mm.finalizeStep(i, 'success');
|
||||
}
|
||||
|
||||
// complete() should have been called for summarization
|
||||
expect(complete).toHaveBeenCalled();
|
||||
|
||||
// After compression, compose should still work
|
||||
const text = mm.compose();
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pruneScreenshots', () => {
|
||||
it('should keep all screenshots when under limit', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img1', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img2', mimeType: 'image/jpeg' }] },
|
||||
];
|
||||
|
||||
const result = mm.pruneScreenshots(messages);
|
||||
expect(result).toEqual(messages); // No pruning — under screenshotKeepCount (3)
|
||||
});
|
||||
|
||||
it('should replace old screenshots with placeholders', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img1', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img2', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img3', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img4', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img5', mimeType: 'image/jpeg' }] },
|
||||
];
|
||||
|
||||
const result = mm.pruneScreenshots(messages);
|
||||
|
||||
// First 2 should be replaced (5 - 3 = keep from index 2)
|
||||
expect((result[0] as any).content[0].type).toBe('text');
|
||||
expect((result[0] as any).content[0].text).toContain('Screenshot removed');
|
||||
expect((result[1] as any).content[0].type).toBe('text');
|
||||
|
||||
// Last 3 should be kept
|
||||
expect((result[2] as any).content[0].type).toBe('image');
|
||||
expect((result[3] as any).content[0].type).toBe('image');
|
||||
expect((result[4] as any).content[0].type).toBe('image');
|
||||
});
|
||||
|
||||
it('should not modify non-image messages', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
||||
{ role: 'user', content: [{ type: 'image', data: 'img1', mimeType: 'image/jpeg' }] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'response' }] },
|
||||
];
|
||||
|
||||
const result = mm.pruneScreenshots(messages);
|
||||
expect(result).toEqual(messages); // Only 1 image, under limit
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose layers', () => {
|
||||
it('should return empty string when no memory', () => {
|
||||
expect(mm.compose()).toBe('');
|
||||
});
|
||||
|
||||
it('should compose all three layers', () => {
|
||||
// We can't easily set summaryMemory directly, but we can verify
|
||||
// pending + recent compose correctly
|
||||
mm.addPendingStep(1, 'Current action');
|
||||
const text = mm.compose();
|
||||
expect(text).toContain('[Current step]');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user