disclaw/tests/integration/channel-queue-router.test.ts
Nick Tabeling 24bd34f886
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
feat(DIS-103): ChannelQueue serializes messages per channel (FIFO)
Add ChannelQueue in src/runtime/channel-queue.ts.
Router enqueues each message so same-channel requests run serially.
Different channels remain parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:49:02 +02:00

115 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
/**
* Integration test: verifies that routeMessage serializes same-channel messages
* through ChannelQueue so they are processed in FIFO order.
*
* runAgent is mocked to record call order without spawning a real process.
* discord.js and the database are also mocked so no real I/O occurs.
*/
// ---- Mock runAgent BEFORE importing router ----
const runOrder: string[] = [];
vi.mock("../../src/agent/runner", () => ({
runAgent: vi.fn(async ({ userMessage }: { userMessage: string }) => {
// Simulate brief async work so the promise chain is exercised
await new Promise<void>((resolve) => setTimeout(resolve, 5));
runOrder.push(userMessage);
return `echo: ${userMessage}`;
}),
}));
// ---- Import after mock registration ----
import { routeMessage } from "../../src/router";
// ---- Helpers ----
/** Builds a minimal fake discord.js Message object. */
function makeMessage(channelId: string, content: string) {
const sent: string[] = [];
const channel = {
name: "test-channel",
sendTyping: vi.fn().mockResolvedValue(undefined),
send: vi.fn(async (text: string) => {
sent.push(text);
return { id: `msg-${Math.random()}` };
}),
};
return {
channelId,
content,
id: `mid-${content}`,
author: { username: "tester" },
channel,
_sent: sent,
} as unknown as import("discord.js").Message & { _sent: string[] };
}
/** Minimal fake DisclawDatabase. */
function makeDb(channelId: string) {
return {
getWorkspaceByChannelId: vi.fn((_id: string) => ({
id: 1,
workspace_path: "/fake/workspace",
})),
getConversationHistory: vi.fn(() => []),
addConversation: vi.fn(),
} as unknown as import("../../src/db/database").DisclawDatabase;
}
/** Minimal fake DisclawConfig. */
const fakeConfig = {} as import("../../src/config/loader").DisclawConfig;
// ---- Tests ----
describe("channel-queue / router integration", () => {
beforeEach(() => {
runOrder.length = 0;
});
afterEach(() => {
vi.restoreAllMocks();
});
it("processes 3 messages in the same channel in FIFO order", async () => {
const channelId = "ch-999";
const db = makeDb(channelId);
// Fire all three without awaiting — they should serialise internally
const p1 = routeMessage(makeMessage(channelId, "first"), db, fakeConfig, null);
const p2 = routeMessage(makeMessage(channelId, "second"), db, fakeConfig, null);
const p3 = routeMessage(makeMessage(channelId, "third"), db, fakeConfig, null);
await Promise.all([p1, p2, p3]);
expect(runOrder).toEqual(["first", "second", "third"]);
});
it("returns true for every handled message", async () => {
const channelId = "ch-handled";
const db = makeDb(channelId);
const results = await Promise.all([
routeMessage(makeMessage(channelId, "a"), db, fakeConfig, null),
routeMessage(makeMessage(channelId, "b"), db, fakeConfig, null),
]);
expect(results).toEqual([true, true]);
});
it("returns false when channel is the management channel", async () => {
const channelId = "ch-mgmt";
const db = makeDb(channelId);
const result = await routeMessage(
makeMessage(channelId, "hi"),
db,
fakeConfig,
channelId // same id => management channel
);
expect(result).toBe(false);
});
});