mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
Closes #136303
## What Problem This Solves
Slack and Google Chat can format a labeled link without a space as `<url|Label>`. Plain-text outbound delivery consumed that input as an autolink and sent the raw `url|Label` text instead of the visible label.
## Root cause
`src/infra/outbound/sanitize-text.ts` applied the angle-bracket autolink replacement before its labeled-link projection. Its URL class included `|`, so the first replacement removed the brackets and hid the label delimiter from the second replacement.
## Fix
Stop the autolink URL class at `|`. The existing shared labeled-link projection then returns the visible label. Ordinary autolinks, mail links, code spans, and HTML stripping keep their existing behavior.
## Evidence
- Current-main red probe at `6190b505d5`: HTTPS and `mailto:` `<url|Label>` inputs leaked `url|Label`; ordinary autolink, code-span, and HTML probes passed.
- Current-main red delivery harness: Google Chat, default Telegram, IRC, and WhatsApp sent the raw URL-plus-label text through the shared outbound boundary.
- Exact-head green delivery test: `test/outbound-sanitize-text-delivery.test.ts` covers Google Chat, default Telegram, IRC, WhatsApp, and direct text/media through the delivery core.
- Exact-head green Telegram integration test: `extensions/telegram/src/send.telegram-http.test.ts` drives the public Telegram plain-text contract and production Telegram sender through a local HTTP boundary.
- Exact-head live Telegram Test Server proof: the leased real-user recorder received one SUT message with exact text `Manual` after the exact production sanitizer sent through the Test Server Bot API.
- Focused tests: 97 sanitizer tests, 41 Google Chat formatter tests, 8 shared outbound delivery tests, and 17 Telegram HTTP integration tests passed.
- Focused oxlint, format, conflict-marker, max-lines, assertion-safety, coercion-helper, and diff checks passed.
Production delta: +2/-2. Test delta: +124/-4. The test growth adds the missing sibling delivery and production Telegram HTTP proof while keeping the contributor's owner-boundary regression coverage.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
183 lines
6.7 KiB
TypeScript
183 lines
6.7 KiB
TypeScript
// Root-owned integration combines shared delivery with public plugin surfaces.
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { googlechatPlugin } from "../extensions/googlechat/api.js";
|
|
import { ircPlugin } from "../extensions/irc/api.js";
|
|
import { telegramOutbound } from "../extensions/telegram/api.js";
|
|
import { whatsappPlugin } from "../extensions/whatsapp/api.js";
|
|
import { createDirectTextMediaOutbound } from "../src/channels/plugins/outbound/direct-text-media.js";
|
|
import type { ChannelOutboundAdapter } from "../src/channels/plugins/types.adapters.js";
|
|
import type { OpenClawConfig } from "../src/config/types.openclaw.js";
|
|
import { deliverOutboundPayloadsCore } from "../src/infra/outbound/deliver-core.js";
|
|
import { prepareOutboundPayloadBatch } from "../src/infra/outbound/deliver-prepare.js";
|
|
import { createEmptyPluginRegistry } from "../src/plugins/registry.js";
|
|
import { setActivePluginRegistry } from "../src/plugins/runtime.js";
|
|
import { createOutboundTestPlugin, createTestRegistry } from "../src/test-utils/channel-plugins.js";
|
|
|
|
const literalCode = '`<p class="literal">code</p>`';
|
|
const fixtures = [
|
|
{
|
|
text: `before<p title="a>b">inside</p>after\n\n${literalCode}`,
|
|
plainText: `before\ninside\nafter\n\n${literalCode}`,
|
|
},
|
|
{
|
|
text: `before<div title='a>b'>inside</div>after\n\n${literalCode}`,
|
|
plainText: `before\ninside\nafter\n\n${literalCode}`,
|
|
},
|
|
{
|
|
text: 'before<a href="`hidden`">click</a> then `visible`',
|
|
plainText: "beforeclick then `visible`",
|
|
},
|
|
];
|
|
const payloads = fixtures.map(({ text }) => ({ text }));
|
|
|
|
const sharedPlainTextSiblings: ReadonlyArray<
|
|
readonly [
|
|
label: string,
|
|
channel: "googlechat" | "irc" | "whatsapp",
|
|
source: Pick<ChannelOutboundAdapter, "sanitizeText" | "normalizePayload">,
|
|
]
|
|
> = [
|
|
[
|
|
"Google Chat",
|
|
"googlechat",
|
|
expectDefined(googlechatPlugin.outbound, "googlechatPlugin.outbound"),
|
|
],
|
|
["IRC", "irc", expectDefined(ircPlugin.outbound, "ircPlugin.outbound")],
|
|
["WhatsApp", "whatsapp", expectDefined(whatsappPlugin.outbound, "whatsappPlugin.outbound")],
|
|
];
|
|
|
|
afterEach(() => {
|
|
setActivePluginRegistry(createEmptyPluginRegistry());
|
|
});
|
|
|
|
describe("HTML sanitization through outbound delivery", () => {
|
|
it.each(["default Telegram", "rich Telegram", "direct text/media"] as const)(
|
|
"preserves the %s transport contract",
|
|
async (mode) => {
|
|
const send = vi.fn(async (_to: string, _text: string) => ({
|
|
messageId: "fixture-message",
|
|
chatId: "12345",
|
|
}));
|
|
const channel = mode === "direct text/media" ? "imessage" : "telegram";
|
|
const cfg: OpenClawConfig =
|
|
mode === "rich Telegram" ? { channels: { telegram: { richMessages: true } } } : {};
|
|
const outbound =
|
|
channel === "telegram"
|
|
? telegramOutbound
|
|
: createDirectTextMediaOutbound({
|
|
channel,
|
|
resolveSender: () => send,
|
|
resolveMaxBytes: () => undefined,
|
|
buildTextOptions: () => ({}),
|
|
buildMediaOptions: () => ({}),
|
|
});
|
|
setActivePluginRegistry(
|
|
createTestRegistry([
|
|
{
|
|
pluginId: channel,
|
|
source: "test",
|
|
plugin: createOutboundTestPlugin({ id: channel, outbound }),
|
|
},
|
|
]),
|
|
);
|
|
const params = { cfg, channel, to: "12345", payloads, deps: { telegram: send } };
|
|
const preparedBatch = await prepareOutboundPayloadBatch(params);
|
|
const results = await deliverOutboundPayloadsCore({ ...params, preparedBatch });
|
|
|
|
expect(results).toHaveLength(payloads.length);
|
|
expect(send.mock.calls.map(([to, text]) => ({ to, text }))).toEqual(
|
|
fixtures.map(({ text, plainText }) => ({
|
|
to: "12345",
|
|
text: mode === "rich Telegram" ? text : plainText,
|
|
})),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(["default Telegram", "direct text/media"] as const)(
|
|
"keeps the unspaced angle-link label on the %s delivery boundary",
|
|
async (mode) => {
|
|
const send = vi.fn(async (_to: string, _text: string) => ({
|
|
messageId: "fixture-message",
|
|
chatId: "12345",
|
|
}));
|
|
const channel = mode === "direct text/media" ? "imessage" : "telegram";
|
|
const outbound =
|
|
channel === "telegram"
|
|
? telegramOutbound
|
|
: createDirectTextMediaOutbound({
|
|
channel,
|
|
resolveSender: () => send,
|
|
resolveMaxBytes: () => undefined,
|
|
buildTextOptions: () => ({}),
|
|
buildMediaOptions: () => ({}),
|
|
});
|
|
setActivePluginRegistry(
|
|
createTestRegistry([
|
|
{
|
|
pluginId: channel,
|
|
source: "test",
|
|
plugin: createOutboundTestPlugin({ id: channel, outbound }),
|
|
},
|
|
]),
|
|
);
|
|
const params = {
|
|
cfg: {} satisfies OpenClawConfig,
|
|
channel,
|
|
to: "12345",
|
|
payloads: [{ text: "<https://example.com/a.pdf|Manual>" }],
|
|
deps: { telegram: send },
|
|
};
|
|
const preparedBatch = await prepareOutboundPayloadBatch(params);
|
|
const results = await deliverOutboundPayloadsCore({ ...params, preparedBatch });
|
|
|
|
expect(results).toHaveLength(1);
|
|
expect(send.mock.calls.map(([to, text]) => ({ to, text }))).toEqual([
|
|
{ to: "12345", text: "Manual" },
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each(sharedPlainTextSiblings)(
|
|
"keeps the unspaced angle-link label on the %s outbound delivery boundary",
|
|
async (_label, channel, source) => {
|
|
const send = vi.fn(
|
|
async (params: Parameters<NonNullable<ChannelOutboundAdapter["sendText"]>>[0]) => ({
|
|
channel,
|
|
messageId: "fixture-message",
|
|
to: params.to,
|
|
text: params.text,
|
|
}),
|
|
);
|
|
const outbound: ChannelOutboundAdapter = {
|
|
deliveryMode: "direct",
|
|
...(source.sanitizeText ? { sanitizeText: source.sanitizeText } : {}),
|
|
...(source.normalizePayload ? { normalizePayload: source.normalizePayload } : {}),
|
|
sendText: send,
|
|
};
|
|
setActivePluginRegistry(
|
|
createTestRegistry([
|
|
{
|
|
pluginId: channel,
|
|
source: "test",
|
|
plugin: createOutboundTestPlugin({ id: channel, outbound }),
|
|
},
|
|
]),
|
|
);
|
|
const params = {
|
|
cfg: {} satisfies OpenClawConfig,
|
|
channel,
|
|
to: "12345",
|
|
payloads: [{ text: "<https://example.com/a.pdf|Manual>" }],
|
|
};
|
|
const preparedBatch = await prepareOutboundPayloadBatch(params);
|
|
const results = await deliverOutboundPayloadsCore({ ...params, preparedBatch });
|
|
|
|
expect(results).toHaveLength(1);
|
|
expect(send.mock.calls.map(([call]) => ({ to: call.to, text: call.text }))).toEqual([
|
|
{ to: "12345", text: "Manual" },
|
|
]);
|
|
},
|
|
);
|
|
});
|