Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
99 lines
2.7 KiB
JavaScript
99 lines
2.7 KiB
JavaScript
/**
|
|
* ConversationLockService Tests
|
|
*
|
|
* Tests for per-conversation agent queue that prevents message interleaving:
|
|
* - Sequential execution within same conversation
|
|
* - Parallel execution across different conversations
|
|
* - Error in one task doesn't block subsequent tasks
|
|
* - Lock cleanup after all tasks complete
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach } from 'vitest';
|
|
|
|
function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Import the class and create fresh instances per test
|
|
let lockService;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('../ConversationLockService.js');
|
|
const singleton = mod.default;
|
|
// Create a fresh instance using the same class
|
|
lockService = new singleton.constructor();
|
|
});
|
|
|
|
describe('ConversationLockService', () => {
|
|
test('sequential execution within same conversation', async () => {
|
|
const order = [];
|
|
|
|
const task1 = lockService.withLock(1, async () => {
|
|
await delay(50);
|
|
order.push('task1');
|
|
return 'result1';
|
|
});
|
|
|
|
const task2 = lockService.withLock(1, async () => {
|
|
order.push('task2');
|
|
return 'result2';
|
|
});
|
|
|
|
const [r1, r2] = await Promise.all([task1, task2]);
|
|
|
|
// task1 must finish before task2 starts (same conversation)
|
|
expect(order).toEqual(['task1', 'task2']);
|
|
expect(r1).toBe('result1');
|
|
expect(r2).toBe('result2');
|
|
});
|
|
|
|
test('parallel execution across different conversations', async () => {
|
|
const order = [];
|
|
|
|
const task1 = lockService.withLock(1, async () => {
|
|
await delay(50);
|
|
order.push('conv1');
|
|
});
|
|
|
|
const task2 = lockService.withLock(2, async () => {
|
|
// No delay — should run immediately since it's a different conversation
|
|
order.push('conv2');
|
|
});
|
|
|
|
await Promise.all([task1, task2]);
|
|
|
|
// conv2 should finish first since it runs in parallel with no delay
|
|
expect(order).toEqual(['conv2', 'conv1']);
|
|
});
|
|
|
|
test('error in one task does not block subsequent tasks', async () => {
|
|
const order = [];
|
|
|
|
const task1 = lockService.withLock(1, async () => {
|
|
order.push('task1');
|
|
throw new Error('task1 failed');
|
|
});
|
|
|
|
const task2 = lockService.withLock(1, async () => {
|
|
order.push('task2');
|
|
return 'ok';
|
|
});
|
|
|
|
await expect(task1).rejects.toThrow('task1 failed');
|
|
const r2 = await task2;
|
|
|
|
expect(order).toEqual(['task1', 'task2']);
|
|
expect(r2).toBe('ok');
|
|
});
|
|
|
|
test('lock cleanup after all tasks complete', async () => {
|
|
await lockService.withLock(42, async () => {
|
|
return 'done';
|
|
});
|
|
|
|
// Allow microtask for cleanup .then to run
|
|
await delay(0);
|
|
|
|
expect(lockService.locks.size).toBe(0);
|
|
});
|
|
});
|