agent-orchestrator/packages/integration-tests/src/notifier-composio.integrati...

189 lines
6.0 KiB
TypeScript

/**
* Integration tests for notifier-composio.
*
* Uses _clientOverride to inject a mock Composio client at the I/O boundary.
* Everything else runs for real: config parsing, tool slug routing, message formatting.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { NotifyAction } from "@composio/ao-core";
import composioPlugin from "@composio/ao-plugin-notifier-composio";
import { makeEvent } from "./helpers/event-factory.js";
const mockExecuteAction = vi.fn().mockResolvedValue({ successful: true });
const mockClient = { executeAction: mockExecuteAction };
describe("notifier-composio integration", () => {
const originalEnv = process.env.COMPOSIO_API_KEY;
beforeEach(() => {
vi.clearAllMocks();
delete process.env.COMPOSIO_API_KEY;
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.COMPOSIO_API_KEY = originalEnv;
} else {
delete process.env.COMPOSIO_API_KEY;
}
});
describe("config -> tool slug routing", () => {
it("slack app routes to SLACK_SEND_MESSAGE with channel", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
defaultApp: "slack",
channelName: "#deploys",
_clientOverride: mockClient,
});
await notifier.notify(makeEvent());
expect(mockExecuteAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "SLACK_SEND_MESSAGE",
params: expect.objectContaining({
channel: "#deploys",
}),
}),
);
});
it("discord app routes to DISCORD_SEND_MESSAGE with channel_id", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
defaultApp: "discord",
channelId: "1234567890",
_clientOverride: mockClient,
});
await notifier.notify(makeEvent());
expect(mockExecuteAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "DISCORD_SEND_MESSAGE",
params: expect.objectContaining({
channel_id: "1234567890",
}),
}),
);
});
it("gmail app routes to GMAIL_SEND_EMAIL with to/subject/body", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
defaultApp: "gmail",
emailTo: "admin@example.com",
_clientOverride: mockClient,
});
await notifier.notify(makeEvent());
expect(mockExecuteAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "GMAIL_SEND_EMAIL",
params: expect.objectContaining({
to: "admin@example.com",
subject: "Agent Orchestrator Notification",
}),
}),
);
});
});
describe("message formatting pipeline", () => {
it("includes priority emoji, event type, session ID, and message", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: mockClient,
});
await notifier.notify(
makeEvent({ priority: "urgent", type: "ci.failing", sessionId: "app-5" }),
);
const text = mockExecuteAction.mock.calls[0][0].params.text as string;
expect(text).toContain("\u{1F6A8}"); // urgent emoji
expect(text).toContain("ci.failing");
expect(text).toContain("app-5");
});
it("includes PR URL when present in event data", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: mockClient,
});
await notifier.notify(makeEvent({ data: { prUrl: "https://github.com/org/repo/pull/99" } }));
const text = mockExecuteAction.mock.calls[0][0].params.text as string;
expect(text).toContain("https://github.com/org/repo/pull/99");
});
it("omits PR URL when not a string", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: mockClient,
});
await notifier.notify(makeEvent({ data: { prUrl: 123 } }));
const text = mockExecuteAction.mock.calls[0][0].params.text as string;
expect(text).not.toContain("PR:");
});
});
describe("notifyWithActions pipeline", () => {
it("includes action labels and URLs in message text", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: mockClient,
});
const actions: NotifyAction[] = [
{ label: "Merge PR", url: "https://github.com/merge" },
{ label: "Kill Session", callbackEndpoint: "/api/kill" },
];
await notifier.notifyWithActions!(makeEvent(), actions);
const text = mockExecuteAction.mock.calls[0][0].params.text as string;
expect(text).toContain("Merge PR: https://github.com/merge");
expect(text).toContain("- Kill Session");
});
});
describe("post pipeline", () => {
it("sends plain text with channel override", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
channelName: "#default",
_clientOverride: mockClient,
});
await notifier.post!("All sessions complete", { channel: "#override" });
const args = mockExecuteAction.mock.calls[0][0].params;
expect(args.text).toBe("All sessions complete");
expect(args.channel).toBe("#override");
});
it("returns null", async () => {
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: mockClient,
});
const result = await notifier.post!("test");
expect(result).toBeNull();
});
});
describe("error handling", () => {
it("unsuccessful result throws descriptive error", async () => {
const failClient = {
executeAction: vi.fn().mockResolvedValue({
successful: false,
error: "Channel not found",
}),
};
const notifier = composioPlugin.create({
composioApiKey: "key",
_clientOverride: failClient,
});
await expect(notifier.notify(makeEvent())).rejects.toThrow("Channel not found");
});
});
});