test: extract message action context coverage
This commit is contained in:
@@ -111,8 +111,9 @@ describe("runMessageAction context isolation", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
it("allows send when target matches current channel", async () => {
|
||||
const result = await runDrySend({
|
||||
it.each([
|
||||
{
|
||||
name: "allows send when target matches current channel",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
@@ -120,39 +121,27 @@ describe("runMessageAction context isolation", () => {
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("accepts legacy to parameter for send", async () => {
|
||||
const result = await runDrySend({
|
||||
},
|
||||
{
|
||||
name: "accepts legacy to parameter for send",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
to: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("defaults to current channel when target is omitted", async () => {
|
||||
const result = await runDrySend({
|
||||
},
|
||||
{
|
||||
name: "defaults to current channel when target is omitted",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("allows media-only send when target matches current channel", async () => {
|
||||
const result = await runDrySend({
|
||||
},
|
||||
{
|
||||
name: "allows media-only send when target matches current channel",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
@@ -160,6 +149,25 @@ describe("runMessageAction context isolation", () => {
|
||||
media: "https://example.com/note.ogg",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
},
|
||||
{
|
||||
name: "allows send when poll booleans are explicitly false",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollMulti: false,
|
||||
pollAnonymous: false,
|
||||
pollPublic: false,
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
},
|
||||
])("$name", async ({ cfg, actionParams, toolContext }) => {
|
||||
const result = await runDrySend({
|
||||
cfg,
|
||||
actionParams,
|
||||
...(toolContext ? { toolContext } : {}),
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
@@ -178,144 +186,111 @@ describe("runMessageAction context isolation", () => {
|
||||
).rejects.toThrow(/message required/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include poll creation params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include string-encoded poll params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollDurationSeconds: "60",
|
||||
pollPublic: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("rejects send actions that include snake_case poll params", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
poll_question: "Ready?",
|
||||
poll_option: ["Yes", "No"],
|
||||
poll_public: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it("allows send when poll booleans are explicitly false", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: slackConfig,
|
||||
it.each([
|
||||
{
|
||||
name: "structured poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollMulti: false,
|
||||
pollAnonymous: false,
|
||||
pollPublic: false,
|
||||
pollQuestion: "Ready?",
|
||||
pollOption: ["Yes", "No"],
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("blocks send when target differs from current channel", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: slackConfig,
|
||||
},
|
||||
{
|
||||
name: "string-encoded poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:C99999999",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollDurationSeconds: "60",
|
||||
pollPublic: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("blocks thread-reply when channelId differs from current channel", async () => {
|
||||
const result = await runDryAction({
|
||||
cfg: slackConfig,
|
||||
action: "thread-reply",
|
||||
},
|
||||
{
|
||||
name: "snake_case poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "C99999999",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
poll_question: "Ready?",
|
||||
poll_option: ["Yes", "No"],
|
||||
poll_public: "true",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("action");
|
||||
},
|
||||
])("rejects send actions that include $name", async ({ actionParams }) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams,
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow(/use action "poll" instead of "send"/i);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp",
|
||||
name: "send when target differs from current slack channel",
|
||||
run: () =>
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
expectedKind: "send",
|
||||
},
|
||||
{
|
||||
name: "thread-reply when channelId differs from current slack channel",
|
||||
run: () =>
|
||||
runDryAction({
|
||||
cfg: slackConfig,
|
||||
action: "thread-reply",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
expectedKind: "action",
|
||||
},
|
||||
])("blocks cross-context UI handoff for $name", async ({ run, expectedKind }) => {
|
||||
const result = await run();
|
||||
expect(result.kind).toBe(expectedKind);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp match",
|
||||
channel: "whatsapp",
|
||||
target: "123@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
},
|
||||
{
|
||||
name: "imessage",
|
||||
name: "imessage match",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551234567",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
},
|
||||
] as const)("allows $name send when target matches current context", async (testCase) => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
channel: testCase.channel,
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: testCase.currentChannelId },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp",
|
||||
name: "whatsapp mismatch",
|
||||
channel: "whatsapp",
|
||||
target: "456@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
},
|
||||
{
|
||||
name: "imessage",
|
||||
name: "imessage mismatch",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551230000",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
currentChannelProvider: "imessage",
|
||||
},
|
||||
] as const)("blocks $name send when target differs from current context", async (testCase) => {
|
||||
] as const)("$name", async (testCase) => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
actionParams: {
|
||||
@@ -325,106 +300,115 @@ describe("runMessageAction context isolation", () => {
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: testCase.currentChannelId,
|
||||
currentChannelProvider: testCase.currentChannelProvider,
|
||||
...(testCase.currentChannelProvider
|
||||
? { currentChannelProvider: testCase.currentChannelProvider }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("infers channel + target from tool context when missing", async () => {
|
||||
const multiConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
it.each([
|
||||
{
|
||||
name: "infers channel + target from tool context when missing",
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
},
|
||||
telegram: {
|
||||
token: "tg-test",
|
||||
},
|
||||
},
|
||||
telegram: {
|
||||
token: "tg-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await runDrySend({
|
||||
cfg: multiConfig,
|
||||
} as OpenClawConfig,
|
||||
action: "send" as const,
|
||||
actionParams: {
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
expect(result.channel).toBe("slack");
|
||||
});
|
||||
|
||||
it("falls back to tool-context provider when channel param is an id", async () => {
|
||||
const result = await runDrySend({
|
||||
expectedKind: "send",
|
||||
expectedChannel: "slack",
|
||||
},
|
||||
{
|
||||
name: "falls back to tool-context provider when channel param is an id",
|
||||
cfg: slackConfig,
|
||||
action: "send" as const,
|
||||
actionParams: {
|
||||
channel: "C12345678",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
expect(result.channel).toBe("slack");
|
||||
});
|
||||
|
||||
it("falls back to tool-context provider for broadcast channel ids", async () => {
|
||||
const result = await runDryAction({
|
||||
expectedKind: "send",
|
||||
expectedChannel: "slack",
|
||||
},
|
||||
{
|
||||
name: "falls back to tool-context provider for broadcast channel ids",
|
||||
cfg: slackConfig,
|
||||
action: "broadcast",
|
||||
action: "broadcast" as const,
|
||||
actionParams: {
|
||||
targets: ["channel:C12345678"],
|
||||
channel: "C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelProvider: "slack" },
|
||||
expectedKind: "broadcast",
|
||||
expectedChannel: "slack",
|
||||
},
|
||||
])("$name", async ({ cfg, action, actionParams, toolContext, expectedKind, expectedChannel }) => {
|
||||
const result = await runDryAction({
|
||||
cfg,
|
||||
action,
|
||||
actionParams,
|
||||
toolContext,
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("broadcast");
|
||||
expect(result.channel).toBe("slack");
|
||||
expect(result.kind).toBe(expectedKind);
|
||||
expect(result.channel).toBe(expectedChannel);
|
||||
});
|
||||
|
||||
it("blocks cross-provider sends by default", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
target: "@opsbot",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
).rejects.toThrow(/Cross-context messaging denied/);
|
||||
});
|
||||
|
||||
it("blocks same-provider cross-context when disabled", async () => {
|
||||
const cfg = {
|
||||
...slackConfig,
|
||||
tools: {
|
||||
message: {
|
||||
crossContext: {
|
||||
allowWithinProvider: false,
|
||||
it.each([
|
||||
{
|
||||
name: "blocks cross-provider sends by default",
|
||||
cfg: slackConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
target: "@opsbot",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
message: /Cross-context messaging denied/,
|
||||
},
|
||||
{
|
||||
name: "blocks same-provider cross-context when disabled",
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
tools: {
|
||||
message: {
|
||||
crossContext: {
|
||||
allowWithinProvider: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
message: /Cross-context messaging denied/,
|
||||
},
|
||||
])("$name", async ({ cfg, actionParams, toolContext, message }) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
actionParams,
|
||||
toolContext,
|
||||
}),
|
||||
).rejects.toThrow(/Cross-context messaging denied/);
|
||||
).rejects.toThrow(message);
|
||||
});
|
||||
|
||||
it.each([
|
||||
Reference in New Issue
Block a user