feat(gateway): complete openclaw phase1 queue parity v2

This commit is contained in:
William Valentin
2026-02-16 12:04:33 -08:00
parent 78da226542
commit 813a0dc5c5
19 changed files with 678 additions and 53 deletions
+75 -6
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { LaneQueue } from './lane-queue.js';
import { LaneQueue, LaneQueueRejectedError } from './lane-queue.js';
describe('LaneQueue', () => {
it('executes a single item immediately', async () => {
@@ -192,8 +192,8 @@ describe('LaneQueue', () => {
expect(r2).toBe('second');
});
it('steer mode keeps only the most recent pending request', async () => {
const queue = new LaneQueue({ mode: 'steer' });
it('followup mode keeps only the most recent pending request', async () => {
const queue = new LaneQueue({ mode: 'followup' });
let resolveFirst!: () => void;
const firstBlocks = new Promise<void>((r) => { resolveFirst = r; });
@@ -204,13 +204,32 @@ describe('LaneQueue', () => {
const p2 = queue.enqueue('lane-a', async () => 'old-pending');
const p3 = queue.enqueue('lane-a', async () => 'latest-pending');
await expect(p2).rejects.toThrow('Superseded by newer request');
await expect(p2).rejects.toThrow('Superseded by newer follow-up request');
resolveFirst();
await expect(p1).resolves.toBe('active');
await expect(p3).resolves.toBe('latest-pending');
});
it('steer_backlog mode replaces existing pending backlog with latest request', async () => {
const queue = new LaneQueue({ mode: 'steer_backlog' });
let resolveFirst!: () => void;
const firstBlocks = new Promise<void>((r) => { resolveFirst = r; });
const p1 = queue.enqueue('lane-a', async () => {
await firstBlocks;
return 'active';
});
const p2 = queue.enqueue('lane-a', async () => 'old-pending');
const p3 = queue.enqueue('lane-a', async () => 'new-pending');
await expect(p2).rejects.toThrow('Superseded by newer request');
resolveFirst();
await expect(p1).resolves.toBe('active');
await expect(p3).resolves.toBe('new-pending');
});
it('drop_new overflow rejects newest request when cap is reached', async () => {
const queue = new LaneQueue({ cap: 1, overflow: 'drop_new' });
let resolveFirst!: () => void;
@@ -223,7 +242,7 @@ describe('LaneQueue', () => {
const p2 = queue.enqueue('lane-a', async () => 'pending-1');
const p3 = queue.enqueue('lane-a', async () => 'pending-2');
await expect(p3).rejects.toThrow('Lane queue full (drop_new)');
await expect(p3).rejects.toThrow('Lane queue full (drop_new): request rejected with 1 pending');
resolveFirst();
await expect(p1).resolves.toBe('active');
@@ -242,7 +261,7 @@ describe('LaneQueue', () => {
const p2 = queue.enqueue('lane-a', async () => 'old-pending');
const p3 = queue.enqueue('lane-a', async () => 'new-pending');
await expect(p2).rejects.toThrow('Lane queue overflow (drop_old)');
await expect(p2).rejects.toThrow('Lane queue overflow (drop_old): oldest pending request dropped');
resolveFirst();
await expect(p1).resolves.toBe('active');
@@ -267,4 +286,54 @@ describe('LaneQueue', () => {
await expect(p1).resolves.toBe('active');
await expect(p3).resolves.toBe('latest-pending');
});
it('applies debounce before starting queued work', async () => {
const queue = new LaneQueue({ debounceMs: 25 });
const events: string[] = [];
let resolveFirst!: () => void;
const firstBlocks = new Promise<void>((r) => { resolveFirst = r; });
const p1 = queue.enqueue('lane-a', async () => {
events.push('active:start');
await firstBlocks;
events.push('active:end');
return 'active';
});
const p2 = queue.enqueue('lane-a', async () => {
events.push('next:start');
return 'next';
});
resolveFirst();
await p1;
expect(queue.isProcessing('lane-a')).toBe(true);
expect(events).toEqual(['active:start', 'active:end']);
await new Promise((r) => setTimeout(r, 40));
await expect(p2).resolves.toBe('next');
expect(events).toEqual(['active:start', 'active:end', 'next:start']);
});
it('returns structured queue rejection errors', async () => {
const queue = new LaneQueue({ cap: 1, overflow: 'drop_new' });
let resolveFirst!: () => void;
const firstBlocks = new Promise<void>((r) => { resolveFirst = r; });
const p1 = queue.enqueue('lane-a', async () => {
await firstBlocks;
return 'active';
});
const p2 = queue.enqueue('lane-a', async () => 'pending');
const p3 = queue.enqueue('lane-a', async () => 'dropped');
const err = await p3.catch((e) => e) as LaneQueueRejectedError;
expect(err).toBeInstanceOf(LaneQueueRejectedError);
expect(err.details.code).toBe('overflow');
expect(err.details.overflow).toBe('drop_new');
expect(err.details.laneId).toBe('lane-a');
resolveFirst();
await expect(p1).resolves.toBe('active');
await expect(p2).resolves.toBe('pending');
});
});