@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
181 lines (147 loc) • 6.38 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import register from "./index.js";
describe("thread-ownership plugin", () => {
const hooks: Record<string, Function> = {};
const api = {
pluginConfig: {},
config: {
agents: {
list: [{ id: "test-agent", default: true, identity: { name: "TestBot" } }],
},
},
id: "thread-ownership",
name: "Thread Ownership",
logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn() },
on: vi.fn((hookName: string, handler: Function) => {
hooks[hookName] = handler;
}),
};
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
vi.clearAllMocks();
for (const key of Object.keys(hooks)) delete hooks[key];
process.env.SLACK_FORWARDER_URL = "http://localhost:8750";
process.env.SLACK_BOT_USER_ID = "U999";
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn() as unknown as typeof globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
delete process.env.SLACK_FORWARDER_URL;
delete process.env.SLACK_BOT_USER_ID;
vi.restoreAllMocks();
});
it("registers message_received and message_sending hooks", () => {
register(api as any);
expect(api.on).toHaveBeenCalledTimes(2);
expect(api.on).toHaveBeenCalledWith("message_received", expect.any(Function));
expect(api.on).toHaveBeenCalledWith("message_sending", expect.any(Function));
});
describe("message_sending", () => {
beforeEach(() => {
register(api as any);
});
it("allows non-slack channels", async () => {
const result = await hooks.message_sending(
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
{ channelId: "discord", conversationId: "C123" },
);
expect(result).toBeUndefined();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it("allows top-level messages (no threadTs)", async () => {
const result = await hooks.message_sending(
{ content: "hello", metadata: {}, to: "C123" },
{ channelId: "slack", conversationId: "C123" },
);
expect(result).toBeUndefined();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it("claims ownership successfully", async () => {
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
);
const result = await hooks.message_sending(
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
{ channelId: "slack", conversationId: "C123" },
);
expect(result).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith(
"http://localhost:8750/api/v1/ownership/C123/1234.5678",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ agent_id: "test-agent" }),
}),
);
});
it("cancels when thread owned by another agent", async () => {
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify({ owner: "other-agent" }), { status: 409 }),
);
const result = await hooks.message_sending(
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
{ channelId: "slack", conversationId: "C123" },
);
expect(result).toEqual({ cancel: true });
expect(api.logger.info).toHaveBeenCalledWith(expect.stringContaining("cancelled send"));
});
it("fails open on network error", async () => {
vi.mocked(globalThis.fetch).mockRejectedValue(new Error("ECONNREFUSED"));
const result = await hooks.message_sending(
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
{ channelId: "slack", conversationId: "C123" },
);
expect(result).toBeUndefined();
expect(api.logger.warn).toHaveBeenCalledWith(
expect.stringContaining("ownership check failed"),
);
});
});
describe("message_received @-mention tracking", () => {
beforeEach(() => {
register(api as any);
});
it("tracks @-mentions and skips ownership check for mentioned threads", async () => {
// Simulate receiving a message that @-mentions the agent.
await hooks.message_received(
{ content: "Hey @TestBot help me", metadata: { threadTs: "9999.0001", channelId: "C456" } },
{ channelId: "slack", conversationId: "C456" },
);
// Now send in the same thread -- should skip the ownership HTTP call.
const result = await hooks.message_sending(
{ content: "Sure!", metadata: { threadTs: "9999.0001", channelId: "C456" }, to: "C456" },
{ channelId: "slack", conversationId: "C456" },
);
expect(result).toBeUndefined();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it("ignores @-mentions on non-slack channels", async () => {
// Use a unique thread key so module-level state from other tests doesn't interfere.
await hooks.message_received(
{ content: "Hey @TestBot", metadata: { threadTs: "7777.0001", channelId: "C999" } },
{ channelId: "discord", conversationId: "C999" },
);
// The mention should not have been tracked, so sending should still call fetch.
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
);
await hooks.message_sending(
{ content: "Sure!", metadata: { threadTs: "7777.0001", channelId: "C999" }, to: "C999" },
{ channelId: "slack", conversationId: "C999" },
);
expect(globalThis.fetch).toHaveBeenCalled();
});
it("tracks bot user ID mentions via <@U999> syntax", async () => {
await hooks.message_received(
{ content: "Hey <@U999> help", metadata: { threadTs: "8888.0001", channelId: "C789" } },
{ channelId: "slack", conversationId: "C789" },
);
const result = await hooks.message_sending(
{ content: "On it!", metadata: { threadTs: "8888.0001", channelId: "C789" }, to: "C789" },
{ channelId: "slack", conversationId: "C789" },
);
expect(result).toBeUndefined();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
});
});