mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 03:52:42 +00:00
refactor(tests): share outbound runner and delivery helpers
This commit is contained in:
@@ -45,6 +45,28 @@ vi.mock("./delivery-queue.js", () => ({
|
|||||||
|
|
||||||
const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js");
|
const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js");
|
||||||
|
|
||||||
|
const telegramChunkConfig: OpenClawConfig = {
|
||||||
|
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const whatsappChunkConfig: OpenClawConfig = {
|
||||||
|
channels: { whatsapp: { textChunkLimit: 4000 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
async function deliverWhatsAppPayload(params: {
|
||||||
|
sendWhatsApp: ReturnType<typeof vi.fn>;
|
||||||
|
payload: { text: string; mediaUrl?: string };
|
||||||
|
cfg?: OpenClawConfig;
|
||||||
|
}) {
|
||||||
|
return deliverOutboundPayloads({
|
||||||
|
cfg: params.cfg ?? whatsappChunkConfig,
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "+1555",
|
||||||
|
payloads: [params.payload],
|
||||||
|
deps: { sendWhatsApp: params.sendWhatsApp },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("deliverOutboundPayloads", () => {
|
describe("deliverOutboundPayloads", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePluginRegistry(defaultRegistry);
|
setActivePluginRegistry(defaultRegistry);
|
||||||
@@ -65,14 +87,11 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
});
|
});
|
||||||
it("chunks telegram markdown and passes through accountId", async () => {
|
it("chunks telegram markdown and passes through accountId", async () => {
|
||||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
||||||
const cfg: OpenClawConfig = {
|
|
||||||
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
|
|
||||||
};
|
|
||||||
const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
process.env.TELEGRAM_BOT_TOKEN = "";
|
process.env.TELEGRAM_BOT_TOKEN = "";
|
||||||
try {
|
try {
|
||||||
const results = await deliverOutboundPayloads({
|
const results = await deliverOutboundPayloads({
|
||||||
cfg,
|
cfg: telegramChunkConfig,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
to: "123",
|
to: "123",
|
||||||
payloads: [{ text: "abcd" }],
|
payloads: [{ text: "abcd" }],
|
||||||
@@ -98,12 +117,9 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("passes explicit accountId to sendTelegram", async () => {
|
it("passes explicit accountId to sendTelegram", async () => {
|
||||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
||||||
const cfg: OpenClawConfig = {
|
|
||||||
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
|
|
||||||
};
|
|
||||||
|
|
||||||
await deliverOutboundPayloads({
|
await deliverOutboundPayloads({
|
||||||
cfg,
|
cfg: telegramChunkConfig,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
to: "123",
|
to: "123",
|
||||||
accountId: "default",
|
accountId: "default",
|
||||||
@@ -120,12 +136,9 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("scopes media local roots to the active agent workspace when agentId is provided", async () => {
|
it("scopes media local roots to the active agent workspace when agentId is provided", async () => {
|
||||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
||||||
const cfg: OpenClawConfig = {
|
|
||||||
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
|
|
||||||
};
|
|
||||||
|
|
||||||
await deliverOutboundPayloads({
|
await deliverOutboundPayloads({
|
||||||
cfg,
|
cfg: telegramChunkConfig,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
to: "123",
|
to: "123",
|
||||||
agentId: "work",
|
agentId: "work",
|
||||||
@@ -251,16 +264,9 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("strips leading blank lines for WhatsApp text payloads", async () => {
|
it("strips leading blank lines for WhatsApp text payloads", async () => {
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
||||||
const cfg: OpenClawConfig = {
|
await deliverWhatsAppPayload({
|
||||||
channels: { whatsapp: { textChunkLimit: 4000 } },
|
sendWhatsApp,
|
||||||
};
|
payload: { text: "\n\nHello from WhatsApp" },
|
||||||
|
|
||||||
await deliverOutboundPayloads({
|
|
||||||
cfg,
|
|
||||||
channel: "whatsapp",
|
|
||||||
to: "+1555",
|
|
||||||
payloads: [{ text: "\n\nHello from WhatsApp" }],
|
|
||||||
deps: { sendWhatsApp },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||||
@@ -274,16 +280,9 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("drops whitespace-only WhatsApp text payloads when no media is attached", async () => {
|
it("drops whitespace-only WhatsApp text payloads when no media is attached", async () => {
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
||||||
const cfg: OpenClawConfig = {
|
const results = await deliverWhatsAppPayload({
|
||||||
channels: { whatsapp: { textChunkLimit: 4000 } },
|
sendWhatsApp,
|
||||||
};
|
payload: { text: " \n\t " },
|
||||||
|
|
||||||
const results = await deliverOutboundPayloads({
|
|
||||||
cfg,
|
|
||||||
channel: "whatsapp",
|
|
||||||
to: "+1555",
|
|
||||||
payloads: [{ text: " \n\t " }],
|
|
||||||
deps: { sendWhatsApp },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||||
@@ -292,16 +291,9 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("keeps WhatsApp media payloads but clears whitespace-only captions", async () => {
|
it("keeps WhatsApp media payloads but clears whitespace-only captions", async () => {
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
||||||
const cfg: OpenClawConfig = {
|
await deliverWhatsAppPayload({
|
||||||
channels: { whatsapp: { textChunkLimit: 4000 } },
|
sendWhatsApp,
|
||||||
};
|
payload: { text: " \n\t ", mediaUrl: "https://example.com/photo.png" },
|
||||||
|
|
||||||
await deliverOutboundPayloads({
|
|
||||||
cfg,
|
|
||||||
channel: "whatsapp",
|
|
||||||
to: "+1555",
|
|
||||||
payloads: [{ text: " \n\t ", mediaUrl: "https://example.com/photo.png" }],
|
|
||||||
deps: { sendWhatsApp },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||||
@@ -504,13 +496,10 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
|
|
||||||
it("mirrors delivered output when mirror options are provided", async () => {
|
it("mirrors delivered output when mirror options are provided", async () => {
|
||||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
||||||
const cfg: OpenClawConfig = {
|
|
||||||
channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } },
|
|
||||||
};
|
|
||||||
mocks.appendAssistantMessageToSessionTranscript.mockClear();
|
mocks.appendAssistantMessageToSessionTranscript.mockClear();
|
||||||
|
|
||||||
await deliverOutboundPayloads({
|
await deliverOutboundPayloads({
|
||||||
cfg,
|
cfg: telegramChunkConfig,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
to: "123",
|
to: "123",
|
||||||
payloads: [{ text: "caption", mediaUrl: "https://example.com/files/report.pdf?sig=1" }],
|
payloads: [{ text: "caption", mediaUrl: "https://example.com/files/report.pdf?sig=1" }],
|
||||||
|
|||||||
@@ -39,6 +39,45 @@ const whatsappConfig = {
|
|||||||
},
|
},
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
async function withSandbox(test: (sandboxDir: string) => Promise<void>) {
|
||||||
|
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
||||||
|
try {
|
||||||
|
await test(sandboxDir);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(sandboxDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runDryAction = (params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
action: "send" | "thread-reply" | "broadcast";
|
||||||
|
actionParams: Record<string, unknown>;
|
||||||
|
toolContext?: Record<string, unknown>;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
sandboxRoot?: string;
|
||||||
|
}) =>
|
||||||
|
runMessageAction({
|
||||||
|
cfg: params.cfg,
|
||||||
|
action: params.action,
|
||||||
|
params: params.actionParams as never,
|
||||||
|
toolContext: params.toolContext as never,
|
||||||
|
dryRun: true,
|
||||||
|
abortSignal: params.abortSignal,
|
||||||
|
sandboxRoot: params.sandboxRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runDrySend = (params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
actionParams: Record<string, unknown>;
|
||||||
|
toolContext?: Record<string, unknown>;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
sandboxRoot?: string;
|
||||||
|
}) =>
|
||||||
|
runDryAction({
|
||||||
|
...params,
|
||||||
|
action: "send",
|
||||||
|
});
|
||||||
|
|
||||||
describe("runMessageAction context isolation", () => {
|
describe("runMessageAction context isolation", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
|
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
|
||||||
@@ -80,62 +119,54 @@ describe("runMessageAction context isolation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows send when target matches current channel", async () => {
|
it("allows send when target matches current channel", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts legacy to parameter for send", async () => {
|
it("accepts legacy to parameter for send", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
to: "#C12345678",
|
to: "#C12345678",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to current channel when target is omitted", async () => {
|
it("defaults to current channel when target is omitted", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows media-only send when target matches current channel", async () => {
|
it("allows media-only send when target matches current channel", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "https://example.com/note.ogg",
|
media: "https://example.com/note.ogg",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
@@ -143,104 +174,92 @@ describe("runMessageAction context isolation", () => {
|
|||||||
|
|
||||||
it("requires message when no media hint is provided", async () => {
|
it("requires message when no media hint is provided", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678" },
|
toolContext: { currentChannelId: "C12345678" },
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/message required/i);
|
).rejects.toThrow(/message required/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks send when target differs from current channel", async () => {
|
it("blocks send when target differs from current channel", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "channel:C99999999",
|
target: "channel:C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks thread-reply when channelId differs from current channel", async () => {
|
it("blocks thread-reply when channelId differs from current channel", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDryAction({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "thread-reply",
|
action: "thread-reply",
|
||||||
params: {
|
actionParams: {
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "C99999999",
|
target: "C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("action");
|
expect(result.kind).toBe("action");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows WhatsApp send when target matches current chat", async () => {
|
it("allows WhatsApp send when target matches current chat", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: whatsappConfig,
|
cfg: whatsappConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
target: "123@g.us",
|
target: "123@g.us",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "123@g.us" },
|
toolContext: { currentChannelId: "123@g.us" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks WhatsApp send when target differs from current chat", async () => {
|
it("blocks WhatsApp send when target differs from current chat", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: whatsappConfig,
|
cfg: whatsappConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
target: "456@g.us",
|
target: "456@g.us",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows iMessage send when target matches current handle", async () => {
|
it("allows iMessage send when target matches current handle", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: whatsappConfig,
|
cfg: whatsappConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
target: "imessage:+15551234567",
|
target: "imessage:+15551234567",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "imessage:+15551234567" },
|
toolContext: { currentChannelId: "imessage:+15551234567" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks iMessage send when target differs from current handle", async () => {
|
it("blocks iMessage send when target differs from current handle", async () => {
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: whatsappConfig,
|
cfg: whatsappConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
target: "imessage:+15551230000",
|
target: "imessage:+15551230000",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
@@ -249,7 +268,6 @@ describe("runMessageAction context isolation", () => {
|
|||||||
currentChannelId: "imessage:+15551234567",
|
currentChannelId: "imessage:+15551234567",
|
||||||
currentChannelProvider: "imessage",
|
currentChannelProvider: "imessage",
|
||||||
},
|
},
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
@@ -268,14 +286,12 @@ describe("runMessageAction context isolation", () => {
|
|||||||
},
|
},
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
|
|
||||||
const result = await runMessageAction({
|
const result = await runDrySend({
|
||||||
cfg: multiConfig,
|
cfg: multiConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
@@ -284,16 +300,14 @@ describe("runMessageAction context isolation", () => {
|
|||||||
|
|
||||||
it("blocks cross-provider sends by default", async () => {
|
it("blocks cross-provider sends by default", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
target: "telegram:@ops",
|
target: "telegram:@ops",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/Cross-context messaging denied/);
|
).rejects.toThrow(/Cross-context messaging denied/);
|
||||||
});
|
});
|
||||||
@@ -311,16 +325,14 @@ describe("runMessageAction context isolation", () => {
|
|||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg,
|
cfg,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "channel:C99999999",
|
target: "channel:C99999999",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/Cross-context messaging denied/);
|
).rejects.toThrow(/Cross-context messaging denied/);
|
||||||
});
|
});
|
||||||
@@ -330,15 +342,13 @@ describe("runMessageAction context isolation", () => {
|
|||||||
controller.abort();
|
controller.abort();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
dryRun: true,
|
|
||||||
abortSignal: controller.signal,
|
abortSignal: controller.signal,
|
||||||
}),
|
}),
|
||||||
).rejects.toMatchObject({ name: "AbortError" });
|
).rejects.toMatchObject({ name: "AbortError" });
|
||||||
@@ -349,15 +359,14 @@ describe("runMessageAction context isolation", () => {
|
|||||||
controller.abort();
|
controller.abort();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDryAction({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "broadcast",
|
action: "broadcast",
|
||||||
params: {
|
actionParams: {
|
||||||
targets: ["channel:C12345678"],
|
targets: ["channel:C12345678"],
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
dryRun: true,
|
|
||||||
abortSignal: controller.signal,
|
abortSignal: controller.signal,
|
||||||
}),
|
}),
|
||||||
).rejects.toMatchObject({ name: "AbortError" });
|
).rejects.toMatchObject({ name: "AbortError" });
|
||||||
@@ -461,8 +470,7 @@ describe("runMessageAction sendAttachment hydration", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
await withSandbox(async (sandboxDir) => {
|
||||||
try {
|
|
||||||
await runMessageAction({
|
await runMessageAction({
|
||||||
cfg,
|
cfg,
|
||||||
action: "sendAttachment",
|
action: "sendAttachment",
|
||||||
@@ -477,9 +485,7 @@ describe("runMessageAction sendAttachment hydration", () => {
|
|||||||
|
|
||||||
const call = vi.mocked(loadWebMedia).mock.calls[0];
|
const call = vi.mocked(loadWebMedia).mock.calls[0];
|
||||||
expect(call?.[0]).toBe(path.join(sandboxDir, "data", "pic.png"));
|
expect(call?.[0]).toBe(path.join(sandboxDir, "data", "pic.png"));
|
||||||
} finally {
|
});
|
||||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -505,106 +511,84 @@ describe("runMessageAction sandboxed media validation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects media outside the sandbox root", async () => {
|
it("rejects media outside the sandbox root", async () => {
|
||||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
await withSandbox(async (sandboxDir) => {
|
||||||
try {
|
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "/etc/passwd",
|
media: "/etc/passwd",
|
||||||
message: "",
|
message: "",
|
||||||
},
|
},
|
||||||
sandboxRoot: sandboxDir,
|
sandboxRoot: sandboxDir,
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/sandbox/i);
|
).rejects.toThrow(/sandbox/i);
|
||||||
} finally {
|
});
|
||||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects file:// media outside the sandbox root", async () => {
|
it("rejects file:// media outside the sandbox root", async () => {
|
||||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
await withSandbox(async (sandboxDir) => {
|
||||||
try {
|
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "file:///etc/passwd",
|
media: "file:///etc/passwd",
|
||||||
message: "",
|
message: "",
|
||||||
},
|
},
|
||||||
sandboxRoot: sandboxDir,
|
sandboxRoot: sandboxDir,
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/sandbox/i);
|
).rejects.toThrow(/sandbox/i);
|
||||||
} finally {
|
});
|
||||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rewrites sandbox-relative media paths", async () => {
|
it("rewrites sandbox-relative media paths", async () => {
|
||||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
await withSandbox(async (sandboxDir) => {
|
||||||
try {
|
const result = await runDrySend({
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "./data/file.txt",
|
media: "./data/file.txt",
|
||||||
message: "",
|
message: "",
|
||||||
},
|
},
|
||||||
sandboxRoot: sandboxDir,
|
sandboxRoot: sandboxDir,
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "file.txt"));
|
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "file.txt"));
|
||||||
} finally {
|
});
|
||||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rewrites MEDIA directives under sandbox", async () => {
|
it("rewrites MEDIA directives under sandbox", async () => {
|
||||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
|
await withSandbox(async (sandboxDir) => {
|
||||||
try {
|
const result = await runDrySend({
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
message: "Hello\nMEDIA: ./data/note.ogg",
|
message: "Hello\nMEDIA: ./data/note.ogg",
|
||||||
},
|
},
|
||||||
sandboxRoot: sandboxDir,
|
sandboxRoot: sandboxDir,
|
||||||
dryRun: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
expect(result.kind).toBe("send");
|
||||||
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "note.ogg"));
|
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "note.ogg"));
|
||||||
} finally {
|
});
|
||||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects data URLs in media params", async () => {
|
it("rejects data URLs in media params", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
runMessageAction({
|
runDrySend({
|
||||||
cfg: slackConfig,
|
cfg: slackConfig,
|
||||||
action: "send",
|
actionParams: {
|
||||||
params: {
|
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
target: "#C12345678",
|
target: "#C12345678",
|
||||||
media: "data:image/png;base64,abcd",
|
media: "data:image/png;base64,abcd",
|
||||||
message: "",
|
message: "",
|
||||||
},
|
},
|
||||||
dryRun: true,
|
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/data:/i);
|
).rejects.toThrow(/data:/i);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user