test: cover thread session routing
This commit is contained in:
@@ -272,4 +272,108 @@ describe("discord tool result dispatch", () => {
|
|||||||
expect(capturedCtx?.ThreadStarterBody).toContain("starter message");
|
expect(capturedCtx?.ThreadStarterBody).toContain("starter message");
|
||||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general");
|
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scopes thread sessions to the routed agent", async () => {
|
||||||
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
|
|
||||||
|
let capturedCtx:
|
||||||
|
| {
|
||||||
|
SessionKey?: string;
|
||||||
|
ParentSessionKey?: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
|
||||||
|
capturedCtx = ctx;
|
||||||
|
dispatcher.sendFinalReply({ text: "hi" });
|
||||||
|
return { queuedFinal: true, counts: { final: 1 } };
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||||
|
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||||
|
messages: { responsePrefix: "PFX" },
|
||||||
|
discord: {
|
||||||
|
dm: { enabled: true, policy: "open" },
|
||||||
|
guilds: { "*": { requireMention: false } },
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
allowFrom: [],
|
||||||
|
bindings: [
|
||||||
|
{ agentId: "support", match: { provider: "discord", guildId: "g1" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||||
|
|
||||||
|
const handler = createDiscordMessageHandler({
|
||||||
|
cfg,
|
||||||
|
token: "token",
|
||||||
|
runtime: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: (code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
botUserId: "bot-id",
|
||||||
|
guildHistories: new Map(),
|
||||||
|
historyLimit: 0,
|
||||||
|
mediaMaxBytes: 10_000,
|
||||||
|
textLimit: 2000,
|
||||||
|
replyToMode: "off",
|
||||||
|
dmEnabled: true,
|
||||||
|
groupDmEnabled: false,
|
||||||
|
guildEntries: { "*": { requireMention: false } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const threadChannel = {
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
name: "thread-name",
|
||||||
|
parentId: "p1",
|
||||||
|
parent: { id: "p1", name: "general" },
|
||||||
|
isThread: () => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
fetchChannel: vi.fn().mockResolvedValue({
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
name: "thread-name",
|
||||||
|
}),
|
||||||
|
rest: {
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
content: "starter message",
|
||||||
|
author: { id: "u1", username: "Alice", discriminator: "0001" },
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as unknown as Client;
|
||||||
|
|
||||||
|
await handler(
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
id: "m5",
|
||||||
|
content: "thread reply",
|
||||||
|
channelId: "t1",
|
||||||
|
channel: threadChannel,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: MessageType.Default,
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
mentionedEveryone: false,
|
||||||
|
mentionedUsers: [],
|
||||||
|
mentionedRoles: [],
|
||||||
|
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
||||||
|
},
|
||||||
|
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
||||||
|
member: { displayName: "Bob" },
|
||||||
|
guild: { id: "g1", name: "Guild" },
|
||||||
|
guild_id: "g1",
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedCtx?.SessionKey).toBe("agent:support:discord:channel:t1");
|
||||||
|
expect(capturedCtx?.ParentSessionKey).toBe(
|
||||||
|
"agent:support:discord:channel:p1",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -388,6 +388,73 @@ describe("monitorSlackProvider tool results", () => {
|
|||||||
expect(ctx.ThreadLabel).toContain("Slack thread #general");
|
expect(ctx.ThreadLabel).toContain("Slack thread #general");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scopes thread session keys to the routed agent", async () => {
|
||||||
|
replyMock.mockResolvedValue({ text: "ok" });
|
||||||
|
config = {
|
||||||
|
messages: { responsePrefix: "PFX" },
|
||||||
|
slack: {
|
||||||
|
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||||
|
channels: { C1: { allow: true, requireMention: false } },
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
allowFrom: [],
|
||||||
|
bindings: [
|
||||||
|
{ agentId: "support", match: { provider: "slack", teamId: "T1" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = getSlackClient();
|
||||||
|
if (client?.auth?.test) {
|
||||||
|
client.auth.test.mockResolvedValue({
|
||||||
|
user_id: "bot-user",
|
||||||
|
team_id: "T1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (client?.conversations?.info) {
|
||||||
|
client.conversations.info.mockResolvedValue({
|
||||||
|
channel: { name: "general", is_channel: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const run = monitorSlackProvider({
|
||||||
|
botToken: "bot-token",
|
||||||
|
appToken: "app-token",
|
||||||
|
abortSignal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForEvent("message");
|
||||||
|
const handler = getSlackHandlers()?.get("message");
|
||||||
|
if (!handler) throw new Error("Slack message handler not registered");
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
event: {
|
||||||
|
type: "message",
|
||||||
|
user: "U1",
|
||||||
|
text: "thread reply",
|
||||||
|
ts: "123.456",
|
||||||
|
thread_ts: "111.222",
|
||||||
|
channel: "C1",
|
||||||
|
channel_type: "channel",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flush();
|
||||||
|
controller.abort();
|
||||||
|
await run;
|
||||||
|
|
||||||
|
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||||
|
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||||
|
SessionKey?: string;
|
||||||
|
ParentSessionKey?: string;
|
||||||
|
};
|
||||||
|
expect(ctx.SessionKey).toBe(
|
||||||
|
"agent:support:slack:channel:C1:thread:111.222",
|
||||||
|
);
|
||||||
|
expect(ctx.ParentSessionKey).toBe("agent:support:slack:channel:C1");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps replies in channel root when message is not threaded", async () => {
|
it("keeps replies in channel root when message is not threaded", async () => {
|
||||||
replyMock.mockResolvedValue({ text: "root reply" });
|
replyMock.mockResolvedValue({ text: "root reply" });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user