refactor: dedupe channel outbound and monitor tests

This commit is contained in:
Peter Steinberger
2026-03-03 00:14:52 +00:00
parent 6a42d09129
commit d7dda4dd1a
18 changed files with 301 additions and 450 deletions

View File

@@ -25,10 +25,18 @@ vi.mock("../auto-reply/dispatch.js", async (importOriginal) => {
};
});
vi.mock("../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
}));
function createPairingStoreMocks() {
return {
readChannelAllowFromStore(...args: unknown[]) {
return readAllowFromStoreMock(...args);
},
upsertChannelPairingRequest(...args: unknown[]) {
return upsertPairingRequestMock(...args);
},
};
}
vi.mock("../pairing/pairing-store.js", () => createPairingStoreMocks());
vi.mock("../config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/sessions.js")>();

View File

@@ -43,8 +43,12 @@ type DiscordReactionEvent = Parameters<MessageReactionAddListener["handle"]>[0];
type DiscordReactionListenerParams = {
cfg: LoadedConfig;
accountId: string;
runtime: RuntimeEnv;
logger: Logger;
onEvent?: () => void;
} & DiscordReactionRoutingParams;
type DiscordReactionRoutingParams = {
botUserId?: string;
dmEnabled: boolean;
groupDmEnabled: boolean;
@@ -54,8 +58,6 @@ type DiscordReactionListenerParams = {
groupPolicy: "open" | "allowlist" | "disabled";
allowNameMatching: boolean;
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
logger: Logger;
onEvent?: () => void;
};
const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000;
@@ -315,23 +317,15 @@ async function authorizeDiscordReactionIngress(
return { allowed: true };
}
async function handleDiscordReactionEvent(params: {
data: DiscordReactionEvent;
client: Client;
action: "added" | "removed";
cfg: LoadedConfig;
accountId: string;
botUserId?: string;
dmEnabled: boolean;
groupDmEnabled: boolean;
groupDmChannels: string[];
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
allowFrom: string[];
groupPolicy: "open" | "allowlist" | "disabled";
allowNameMatching: boolean;
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
logger: Logger;
}) {
async function handleDiscordReactionEvent(
params: {
data: DiscordReactionEvent;
client: Client;
action: "added" | "removed";
cfg: LoadedConfig;
logger: Logger;
} & DiscordReactionRoutingParams,
) {
try {
const { data, client, action, botUserId, guildEntries } = params;
if (!("user" in data)) {

View File

@@ -120,6 +120,19 @@ const { processDiscordMessage } = await import("./message-handler.process.js");
const createBaseContext = createBaseDiscordMessageContext;
function mockDispatchSingleBlockReply(payload: { text: string; isReasoning?: boolean }) {
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.dispatcher.sendBlockReply(payload);
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 1 } };
});
}
async function processStreamOffDiscordMessage() {
const ctx = await createBaseContext({ discordConfig: { streamMode: "off" } });
// oxlint-disable-next-line typescript/no-explicit-any
await processDiscordMessage(ctx as any);
}
beforeEach(() => {
vi.useRealTimers();
sendMocks.reactMessageDiscord.mockClear();
@@ -463,15 +476,8 @@ describe("processDiscordMessage draft streaming", () => {
});
it("suppresses reasoning payload delivery to Discord", async () => {
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.dispatcher.sendBlockReply({ text: "thinking...", isReasoning: true });
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 1 } };
});
const ctx = await createBaseContext({ discordConfig: { streamMode: "off" } });
// oxlint-disable-next-line typescript/no-explicit-any
await processDiscordMessage(ctx as any);
mockDispatchSingleBlockReply({ text: "thinking...", isReasoning: true });
await processStreamOffDiscordMessage();
expect(deliverDiscordReply).not.toHaveBeenCalled();
});
@@ -495,15 +501,8 @@ describe("processDiscordMessage draft streaming", () => {
});
it("delivers non-reasoning block payloads to Discord", async () => {
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.dispatcher.sendBlockReply({ text: "hello from block stream" });
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 1 } };
});
const ctx = await createBaseContext({ discordConfig: { streamMode: "off" } });
// oxlint-disable-next-line typescript/no-explicit-any
await processDiscordMessage(ctx as any);
mockDispatchSingleBlockReply({ text: "hello from block stream" });
await processStreamOffDiscordMessage();
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
});

View File

@@ -210,8 +210,10 @@ function createBoundThreadBindingManager(params: {
targetSessionKey: string;
agentId: string;
}): ThreadBindingManager {
const baseManager = createNoopThreadBindingManager(params.accountId);
const now = Date.now();
return {
accountId: params.accountId,
...baseManager,
getIdleTimeoutMs: () => 24 * 60 * 60 * 1000,
getMaxAgeMs: () => 0,
getByThreadId: (threadId: string) =>
@@ -224,20 +226,12 @@ function createBoundThreadBindingManager(params: {
targetSessionKey: params.targetSessionKey,
agentId: params.agentId,
boundBy: "system",
boundAt: Date.now(),
lastActivityAt: Date.now(),
boundAt: now,
lastActivityAt: now,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
}
: undefined,
getBySessionKey: () => undefined,
listBySessionKey: () => [],
listBindings: () => [],
touchThread: () => null,
bindTarget: async () => null,
unbindThread: () => null,
unbindBySessionKey: () => [],
stop: () => {},
: baseManager.getByThreadId(threadId),
};
}

View File

@@ -258,6 +258,14 @@ describe("monitorDiscordProvider", () => {
},
}) as OpenClawConfig;
const getConstructedEventQueue = (): { listenerTimeout?: number } | undefined => {
expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1);
const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as {
eventQueue?: { listenerTimeout?: number };
};
return opts.eventQueue;
};
beforeEach(() => {
clientConstructorOptionsMock.mockClear();
clientFetchUserMock.mockClear().mockResolvedValue({ id: "bot-1" });
@@ -349,12 +357,9 @@ describe("monitorDiscordProvider", () => {
runtime: baseRuntime(),
});
expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1);
const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as {
eventQueue?: { listenerTimeout?: number };
};
expect(opts.eventQueue).toBeDefined();
expect(opts.eventQueue?.listenerTimeout).toBe(120_000);
const eventQueue = getConstructedEventQueue();
expect(eventQueue).toBeDefined();
expect(eventQueue?.listenerTimeout).toBe(120_000);
});
it("forwards custom eventQueue config from discord config to Carbon Client", async () => {
@@ -377,10 +382,7 @@ describe("monitorDiscordProvider", () => {
runtime: baseRuntime(),
});
expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1);
const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as {
eventQueue?: { listenerTimeout?: number };
};
expect(opts.eventQueue?.listenerTimeout).toBe(300_000);
const eventQueue = getConstructedEventQueue();
expect(eventQueue?.listenerTimeout).toBe(300_000);
});
});