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
+80 -3
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { GatewayEvent, GatewayRequest, OutboundMessage } from '../protocol.js';
import { LaneQueue } from '../lane-queue.js';
import { LaneQueue, LaneQueueRejectedError } from '../lane-queue.js';
import { createAgentHandlers } from './agent.js';
import type { AgentHandlerDeps } from './agent.js';
import { CommandRegistry, registerBuiltinCommands } from '../../commands/index.js';
@@ -28,6 +28,7 @@ describe('createAgentHandlers command fast-path', () => {
};
const sessionManager = {
getSessionConfig: vi.fn(),
setSessionConfig: vi.fn(),
deleteSessionConfig: vi.fn(),
};
@@ -123,6 +124,26 @@ describe('createAgentHandlers command fast-path', () => {
expect((sent[0] as GatewayEvent).event).toBe('done');
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toBe('agent response');
});
it('handles /queue command via fast-path and persists queue session config', async () => {
const sent: OutboundMessage[] = [];
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
const req: GatewayRequest = {
id: 5,
method: 'agent.send',
params: {
message: '/queue set mode followup',
connectionId: 'conn-1',
metadata: { isCommand: true, command: 'queue', commandArgs: 'set mode followup' },
},
};
await handlers['agent.send'](req, send);
expect(sessionManager.setSessionConfig).toHaveBeenCalledWith('ws', 'ws:conn-1', 'queue.mode', 'followup');
expect(mockAgent.process).not.toHaveBeenCalled();
expect(((sent[0] as GatewayEvent).data as { content: string }).content).toContain('Set queue.mode=followup');
});
});
describe('createAgentHandlers queue policy resolution', () => {
@@ -154,7 +175,7 @@ describe('createAgentHandlers queue policy resolution', () => {
cancel: vi.fn(),
} as unknown as LaneQueue;
const resolveQueuePolicy = vi.fn(() => ({ mode: 'steer' as const, cap: 3 }));
const resolveQueuePolicy = vi.fn(() => ({ mode: 'steer_backlog' as const, cap: 3, debounceMs: 25 }));
const handlers = createAgentHandlers({
sessionBridge: sessionBridge as unknown as AgentHandlerDeps['sessionBridge'],
@@ -178,8 +199,64 @@ describe('createAgentHandlers queue policy resolution', () => {
channel: 'ws',
});
expect((laneQueue.enqueue as unknown as ReturnType<typeof vi.fn>).mock.calls[0][2]).toEqual({
mode: 'steer',
mode: 'steer_backlog',
cap: 3,
debounceMs: 25,
});
});
it('emits structured queue error events for lane rejections', async () => {
const sessionBridge = {
getAgent: vi.fn(() => ({
process: vi.fn(async () => 'ok'),
getUsage: vi.fn(() => ({
primary: { inputTokens: 0, outputTokens: 0, calls: 0 },
delegation: {},
total: { inputTokens: 0, outputTokens: 0, calls: 0, estimatedCost: 0 },
})),
getModelTier: vi.fn(() => 'default'),
setModelTier: vi.fn(),
compact: vi.fn(async () => null),
reset: vi.fn(),
})),
getSessionId: vi.fn(() => 'ws:s1'),
setBusy: vi.fn(),
setOnToolUse: vi.fn(),
isBusy: vi.fn(() => false),
};
const laneQueue = {
enqueue: vi.fn(async () => {
throw new LaneQueueRejectedError({
code: 'overflow',
laneId: 'ws:s1',
mode: 'followup',
overflow: 'drop_new',
droppedCount: 1,
message: 'Lane queue full (drop_new)',
});
}),
cancel: vi.fn(),
} as unknown as LaneQueue;
const handlers = createAgentHandlers({
sessionBridge: sessionBridge as unknown as AgentHandlerDeps['sessionBridge'],
laneQueue,
});
const sent: OutboundMessage[] = [];
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
await handlers['agent.send']({
id: 6,
method: 'agent.send',
params: { message: 'hello', connectionId: 'conn-1' },
}, send);
expect(sent).toHaveLength(1);
const event = sent[0] as GatewayEvent;
expect(event.event).toBe('error');
expect((event.data as { code: number }).code).toBe(3);
expect((event.data as { queue?: { code: string } }).queue?.code).toBe('overflow');
});
});