diff --git a/src/infra/outbound/outbound-policy.test.ts b/src/infra/outbound/outbound-policy.test.ts new file mode 100644 index 00000000000..fd19649c345 --- /dev/null +++ b/src/infra/outbound/outbound-policy.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../config/config.js"; +import { + applyCrossContextDecoration, + buildCrossContextDecoration, + enforceCrossContextPolicy, + shouldApplyCrossContextMarker, +} from "./outbound-policy.js"; + +const slackConfig = { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + }, + }, +} as OpenClawConfig; + +const discordConfig = { + channels: { + discord: {}, + }, +} as OpenClawConfig; + +describe("outbound policy helpers", () => { + it("allows cross-provider sends when enabled", () => { + const cfg = { + ...slackConfig, + tools: { + message: { crossContext: { allowAcrossProviders: true } }, + }, + } as OpenClawConfig; + + expect(() => + enforceCrossContextPolicy({ + cfg, + channel: "telegram", + action: "send", + args: { to: "telegram:@ops" }, + toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, + }), + ).not.toThrow(); + }); + + it("blocks cross-provider sends when not allowed", () => { + expect(() => + enforceCrossContextPolicy({ + cfg: slackConfig, + channel: "telegram", + action: "send", + args: { to: "telegram:@ops" }, + toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, + }), + ).toThrow(/target provider "telegram" while bound to "slack"/); + }); + + it("blocks same-provider cross-context sends when allowWithinProvider is false", () => { + const cfg = { + ...slackConfig, + tools: { + message: { crossContext: { allowWithinProvider: false } }, + }, + } as OpenClawConfig; + + expect(() => + enforceCrossContextPolicy({ + cfg, + channel: "slack", + action: "send", + args: { to: "C999" }, + toolContext: { currentChannelId: "C123", currentChannelProvider: "slack" }, + }), + ).toThrow(/target="C999" while bound to "C123"/); + }); + + it("uses components when available and preferred", async () => { + const decoration = await buildCrossContextDecoration({ + cfg: discordConfig, + channel: "discord", + target: "123", + toolContext: { currentChannelId: "C12345678", currentChannelProvider: "discord" }, + }); + + expect(decoration).not.toBeNull(); + const applied = applyCrossContextDecoration({ + message: "hello", + decoration: decoration!, + preferComponents: true, + }); + + expect(applied.usedComponents).toBe(true); + expect(applied.componentsBuilder).toBeDefined(); + expect(applied.componentsBuilder?.("hello").length).toBeGreaterThan(0); + expect(applied.message).toBe("hello"); + }); + + it("returns null when decoration is skipped and falls back to text markers", async () => { + await expect( + buildCrossContextDecoration({ + cfg: discordConfig, + channel: "discord", + target: "123", + toolContext: { + currentChannelId: "C12345678", + currentChannelProvider: "discord", + skipCrossContextDecoration: true, + }, + }), + ).resolves.toBeNull(); + + const applied = applyCrossContextDecoration({ + message: "hello", + decoration: { prefix: "[from ops] ", suffix: " [cc]" }, + preferComponents: true, + }); + expect(applied).toEqual({ + message: "[from ops] hello [cc]", + usedComponents: false, + }); + }); + + it("marks only supported cross-context actions", () => { + expect(shouldApplyCrossContextMarker("send")).toBe(true); + expect(shouldApplyCrossContextMarker("thread-reply")).toBe(true); + expect(shouldApplyCrossContextMarker("thread-create")).toBe(false); + }); +}); diff --git a/src/infra/outbound/outbound.test.ts b/src/infra/outbound/outbound.test.ts index c7f55363350..84a38298145 100644 --- a/src/infra/outbound/outbound.test.ts +++ b/src/infra/outbound/outbound.test.ts @@ -16,11 +16,6 @@ import { moveToFailed, recoverPendingDeliveries, } from "./delivery-queue.js"; -import { - applyCrossContextDecoration, - buildCrossContextDecoration, - enforceCrossContextPolicy, -} from "./outbound-policy.js"; import { resolveOutboundSessionRoute } from "./outbound-session.js"; import { runResolveOutboundTargetCoreTests } from "./targets.shared-test.js"; @@ -603,63 +598,6 @@ describe("delivery-queue", () => { }); }); -const slackConfig = { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - }, - }, -} as OpenClawConfig; - -const discordConfig = { - channels: { - discord: {}, - }, -} as OpenClawConfig; - -describe("outbound policy", () => { - it("allows cross-provider sends when enabled", () => { - const cfg = { - ...slackConfig, - tools: { - message: { crossContext: { allowAcrossProviders: true } }, - }, - } as OpenClawConfig; - - expect(() => - enforceCrossContextPolicy({ - cfg, - channel: "telegram", - action: "send", - args: { to: "telegram:@ops" }, - toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - }), - ).not.toThrow(); - }); - - it("uses components when available and preferred", async () => { - const decoration = await buildCrossContextDecoration({ - cfg: discordConfig, - channel: "discord", - target: "123", - toolContext: { currentChannelId: "C12345678", currentChannelProvider: "discord" }, - }); - - expect(decoration).not.toBeNull(); - const applied = applyCrossContextDecoration({ - message: "hello", - decoration: decoration!, - preferComponents: true, - }); - - expect(applied.usedComponents).toBe(true); - expect(applied.componentsBuilder).toBeDefined(); - expect(applied.componentsBuilder?.("hello").length).toBeGreaterThan(0); - expect(applied.message).toBe("hello"); - }); -}); - describe("resolveOutboundSessionRoute", () => { const baseConfig = {} as OpenClawConfig;