test: optimize gateway infra memory and security coverage

This commit is contained in:
Peter Steinberger
2026-02-21 21:43:20 +00:00
parent 58254b3b57
commit cc2ff68947
24 changed files with 1163 additions and 1284 deletions

View File

@@ -17,37 +17,26 @@ describe("format-duration", () => {
expect(formatDurationCompact(-100)).toBeUndefined();
});
it("formats milliseconds for sub-second durations", () => {
expect(formatDurationCompact(500)).toBe("500ms");
expect(formatDurationCompact(999)).toBe("999ms");
});
it("formats seconds", () => {
expect(formatDurationCompact(1000)).toBe("1s");
expect(formatDurationCompact(45000)).toBe("45s");
expect(formatDurationCompact(59000)).toBe("59s");
});
it("formats minutes and seconds", () => {
expect(formatDurationCompact(60000)).toBe("1m");
expect(formatDurationCompact(65000)).toBe("1m5s");
expect(formatDurationCompact(90000)).toBe("1m30s");
});
it("omits trailing zero components", () => {
expect(formatDurationCompact(60000)).toBe("1m"); // not "1m0s"
expect(formatDurationCompact(3600000)).toBe("1h"); // not "1h0m"
expect(formatDurationCompact(86400000)).toBe("1d"); // not "1d0h"
});
it("formats hours and minutes", () => {
expect(formatDurationCompact(3660000)).toBe("1h1m");
expect(formatDurationCompact(5400000)).toBe("1h30m");
});
it("formats days and hours", () => {
expect(formatDurationCompact(90000000)).toBe("1d1h");
expect(formatDurationCompact(172800000)).toBe("2d");
it("formats compact units and omits trailing zero components", () => {
const cases = [
[500, "500ms"],
[999, "999ms"],
[1000, "1s"],
[45000, "45s"],
[59000, "59s"],
[60000, "1m"], // not "1m0s"
[65000, "1m5s"],
[90000, "1m30s"],
[3600000, "1h"], // not "1h0m"
[3660000, "1h1m"],
[5400000, "1h30m"],
[86400000, "1d"], // not "1d0h"
[90000000, "1d1h"],
[172800000, "2d"],
] as const;
for (const [input, expected] of cases) {
expect(formatDurationCompact(input), String(input)).toBe(expected);
}
});
it("supports spaced option", () => {
@@ -65,25 +54,27 @@ describe("format-duration", () => {
});
describe("formatDurationHuman", () => {
it("returns fallback for invalid input", () => {
it("returns fallback for invalid duration input", () => {
for (const value of [null, undefined, -100]) {
expect(formatDurationHuman(value)).toBe("n/a");
}
expect(formatDurationHuman(null, "unknown")).toBe("unknown");
});
it("formats single unit", () => {
expect(formatDurationHuman(500)).toBe("500ms");
expect(formatDurationHuman(5000)).toBe("5s");
expect(formatDurationHuman(180000)).toBe("3m");
expect(formatDurationHuman(7200000)).toBe("2h");
expect(formatDurationHuman(172800000)).toBe("2d");
});
it("uses 24h threshold for days", () => {
expect(formatDurationHuman(23 * 3600000)).toBe("23h");
expect(formatDurationHuman(24 * 3600000)).toBe("1d");
expect(formatDurationHuman(25 * 3600000)).toBe("1d"); // rounds
it("formats single-unit outputs and day threshold behavior", () => {
const cases = [
[500, "500ms"],
[5000, "5s"],
[180000, "3m"],
[7200000, "2h"],
[23 * 3600000, "23h"],
[24 * 3600000, "1d"],
[25 * 3600000, "1d"], // rounds
[172800000, "2d"],
] as const;
for (const [input, expected] of cases) {
expect(formatDurationHuman(input), String(input)).toBe(expected);
}
});
});
@@ -166,20 +157,27 @@ describe("format-datetime", () => {
describe("format-relative", () => {
describe("formatTimeAgo", () => {
it("returns fallback for invalid input", () => {
it("returns fallback for invalid elapsed input", () => {
for (const value of [null, undefined, -100]) {
expect(formatTimeAgo(value)).toBe("unknown");
}
expect(formatTimeAgo(null, { fallback: "n/a" })).toBe("n/a");
});
it("formats with 'ago' suffix by default", () => {
expect(formatTimeAgo(0)).toBe("just now");
expect(formatTimeAgo(29000)).toBe("just now"); // rounds to <1m
expect(formatTimeAgo(30000)).toBe("1m ago"); // 30s rounds to 1m
expect(formatTimeAgo(300000)).toBe("5m ago");
expect(formatTimeAgo(7200000)).toBe("2h ago");
expect(formatTimeAgo(172800000)).toBe("2d ago");
it("formats relative age around key unit boundaries", () => {
const cases = [
[0, "just now"],
[29000, "just now"], // rounds to <1m
[30000, "1m ago"], // 30s rounds to 1m
[300000, "5m ago"],
[7200000, "2h ago"],
[47 * 3600000, "47h ago"],
[48 * 3600000, "2d ago"],
[172800000, "2d ago"],
] as const;
for (const [input, expected] of cases) {
expect(formatTimeAgo(input), String(input)).toBe(expected);
}
});
it("omits suffix when suffix: false", () => {
@@ -187,15 +185,10 @@ describe("format-relative", () => {
expect(formatTimeAgo(300000, { suffix: false })).toBe("5m");
expect(formatTimeAgo(7200000, { suffix: false })).toBe("2h");
});
it("uses 48h threshold before switching to days", () => {
expect(formatTimeAgo(47 * 3600000)).toBe("47h ago");
expect(formatTimeAgo(48 * 3600000)).toBe("2d ago");
});
});
describe("formatRelativeTimestamp", () => {
it("returns fallback for invalid input", () => {
it("returns fallback for invalid timestamp input", () => {
for (const value of [null, undefined]) {
expect(formatRelativeTimestamp(value)).toBe("n/a");
}

View File

@@ -168,15 +168,19 @@ describe("resolveHeartbeatIntervalMs", () => {
});
describe("resolveHeartbeatPrompt", () => {
it("uses the default prompt when unset", () => {
expect(resolveHeartbeatPrompt({})).toBe(HEARTBEAT_PROMPT);
});
it("uses a trimmed override when configured", () => {
const cfg: OpenClawConfig = {
agents: { defaults: { heartbeat: { prompt: " ping " } } },
};
expect(resolveHeartbeatPrompt(cfg)).toBe("ping");
it("uses default or trimmed override prompts", () => {
const cases = [
{ cfg: {} as OpenClawConfig, expected: HEARTBEAT_PROMPT },
{
cfg: {
agents: { defaults: { heartbeat: { prompt: " ping " } } },
} as OpenClawConfig,
expected: "ping",
},
] as const;
for (const testCase of cases) {
expect(resolveHeartbeatPrompt(testCase.cfg)).toBe(testCase.expected);
}
});
});
@@ -323,67 +327,61 @@ describe("resolveHeartbeatDeliveryTarget", () => {
});
});
it("parses threadId from :topic: suffix in heartbeat to", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "-100111:topic:42" },
it("parses optional telegram :topic: threadId suffix", () => {
const cases = [
{ to: "-100111:topic:42", expectedTo: "-100111", expectedThreadId: 42 },
{ to: "-100111", expectedTo: "-100111", expectedThreadId: undefined },
] as const;
for (const testCase of cases) {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: testCase.to },
},
},
},
};
const result = resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry });
expect(result.channel).toBe("telegram");
expect(result.to).toBe("-100111");
expect(result.threadId).toBe(42);
};
const result = resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry });
expect(result.channel).toBe("telegram");
expect(result.to).toBe(testCase.expectedTo);
expect(result.threadId).toBe(testCase.expectedThreadId);
}
});
it("heartbeat to without :topic: has no threadId", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "-100111" },
it("handles explicit heartbeat accountId allow/deny", () => {
const cases = [
{
accountId: "work",
expected: {
channel: "telegram",
to: "123",
accountId: "work",
lastChannel: undefined,
lastAccountId: undefined,
},
},
};
const result = resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry });
expect(result.to).toBe("-100111");
expect(result.threadId).toBeUndefined();
});
{
accountId: "missing",
expected: {
channel: "none",
reason: "unknown-account",
accountId: "missing",
lastChannel: undefined,
lastAccountId: undefined,
},
},
] as const;
it("uses explicit heartbeat accountId when provided", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "123", accountId: "work" },
for (const testCase of cases) {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "123", accountId: testCase.accountId },
},
},
},
channels: { telegram: { accounts: { work: { botToken: "token" } } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({
channel: "telegram",
to: "123",
accountId: "work",
lastChannel: undefined,
lastAccountId: undefined,
});
});
it("skips when explicit heartbeat accountId is unknown", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "123", accountId: "missing" },
},
},
channels: { telegram: { accounts: { work: { botToken: "token" } } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({
channel: "none",
reason: "unknown-account",
accountId: "missing",
lastChannel: undefined,
lastAccountId: undefined,
});
channels: { telegram: { accounts: { work: { botToken: "token" } } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual(testCase.expected);
}
});
it("prefers per-agent heartbeat overrides when provided", () => {

View File

@@ -15,37 +15,22 @@ function okResponse(body = "ok"): Response {
describe("fetchWithSsrFGuard hardening", () => {
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
it("blocks private IP literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://127.0.0.1:8080/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("blocks legacy loopback literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://0177.0.0.1:8080/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("blocks unsupported packed-hex loopback literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url: "http://0x7f000001/internal",
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
it("blocks private and legacy loopback literals before fetch", async () => {
const blockedUrls = [
"http://127.0.0.1:8080/internal",
"http://0177.0.0.1:8080/internal",
"http://0x7f000001/internal",
];
for (const url of blockedUrls) {
const fetchImpl = vi.fn();
await expect(
fetchWithSsrFGuard({
url,
fetchImpl,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
}
});
it("blocks redirect chains that hop to private hosts", async () => {

View File

@@ -59,27 +59,23 @@ const unsupportedLegacyIpv4Cases = [
const nonIpHostnameCases = ["example.com", "abc.123.example", "1password.com", "0x.example.com"];
describe("ssrf ip classification", () => {
it.each(privateIpCases)("classifies %s as private", (address) => {
expect(isPrivateIpAddress(address)).toBe(true);
});
it.each(publicIpCases)("classifies %s as public", (address) => {
expect(isPrivateIpAddress(address)).toBe(false);
});
it.each(malformedIpv6Cases)("fails closed for malformed IPv6 %s", (address) => {
expect(isPrivateIpAddress(address)).toBe(true);
});
it.each(unsupportedLegacyIpv4Cases)(
"fails closed for unsupported legacy IPv4 literal %s",
(address) => {
it("classifies blocked ip literals as private", () => {
const blockedCases = [...privateIpCases, ...malformedIpv6Cases, ...unsupportedLegacyIpv4Cases];
for (const address of blockedCases) {
expect(isPrivateIpAddress(address)).toBe(true);
},
);
}
});
it.each(nonIpHostnameCases)("does not treat hostname %s as an IP literal", (hostname) => {
expect(isPrivateIpAddress(hostname)).toBe(false);
it("classifies public ip literals as non-private", () => {
for (const address of publicIpCases) {
expect(isPrivateIpAddress(address)).toBe(false);
}
});
it("does not treat hostnames as ip literals", () => {
for (const hostname of nonIpHostnameCases) {
expect(isPrivateIpAddress(hostname)).toBe(false);
}
});
});

View File

@@ -124,8 +124,6 @@ describe("resolveOpenClawPackageRoot", () => {
});
it("falls back when argv1 realpath throws", async () => {
const { resolveOpenClawPackageRootSync } = await import("./openclaw-root.js");
const project = fx("realpath-throw-scenario");
const argv1 = path.join(project, "node_modules", ".bin", "openclaw");
const pkgRoot = path.join(project, "node_modules", "openclaw");
@@ -158,8 +156,6 @@ describe("resolveOpenClawPackageRoot", () => {
});
it("async resolver returns null when no package roots exist", async () => {
const { resolveOpenClawPackageRoot } = await import("./openclaw-root.js");
await expect(resolveOpenClawPackageRoot({ cwd: fx("missing") })).resolves.toBeNull();
});
});

View File

@@ -161,17 +161,22 @@ describe("delivery-queue", () => {
});
describe("computeBackoffMs", () => {
it("returns 0 for retryCount 0", () => {
expect(computeBackoffMs(0)).toBe(0);
});
it("returns scheduled backoff values and clamps at max retry", () => {
const cases = [
{ retryCount: 0, expected: 0 },
{ retryCount: 1, expected: 5_000 },
{ retryCount: 2, expected: 25_000 },
{ retryCount: 3, expected: 120_000 },
{ retryCount: 4, expected: 600_000 },
// Beyond defined schedule -- clamps to last value.
{ retryCount: 5, expected: 600_000 },
] as const;
it("returns correct backoff for each retry", () => {
expect(computeBackoffMs(1)).toBe(5_000);
expect(computeBackoffMs(2)).toBe(25_000);
expect(computeBackoffMs(3)).toBe(120_000);
expect(computeBackoffMs(4)).toBe(600_000);
// Beyond defined schedule -- clamps to last value.
expect(computeBackoffMs(5)).toBe(600_000);
for (const testCase of cases) {
expect(computeBackoffMs(testCase.retryCount), String(testCase.retryCount)).toBe(
testCase.expected,
);
}
});
});
@@ -383,28 +388,36 @@ describe("DirectoryCache", () => {
expect(cache.get("a", cfg)).toBeUndefined();
});
it("evicts oldest keys when max size is exceeded", () => {
const cache = new DirectoryCache<string>(60_000, 2);
cache.set("a", "value-a", cfg);
cache.set("b", "value-b", cfg);
cache.set("c", "value-c", cfg);
it("evicts least-recent entries when capacity is exceeded", () => {
const cases = [
{
actions: [
["set", "a", "value-a"],
["set", "b", "value-b"],
["set", "c", "value-c"],
] as const,
expected: { a: undefined, b: "value-b", c: "value-c" },
},
{
actions: [
["set", "a", "value-a"],
["set", "b", "value-b"],
["set", "a", "value-a2"],
["set", "c", "value-c"],
] as const,
expected: { a: "value-a2", b: undefined, c: "value-c" },
},
] as const;
expect(cache.get("a", cfg)).toBeUndefined();
expect(cache.get("b", cfg)).toBe("value-b");
expect(cache.get("c", cfg)).toBe("value-c");
});
it("refreshes insertion order on key updates", () => {
const cache = new DirectoryCache<string>(60_000, 2);
cache.set("a", "value-a", cfg);
cache.set("b", "value-b", cfg);
cache.set("a", "value-a2", cfg);
cache.set("c", "value-c", cfg);
// Updating "a" should keep it and evict older "b".
expect(cache.get("a", cfg)).toBe("value-a2");
expect(cache.get("b", cfg)).toBeUndefined();
expect(cache.get("c", cfg)).toBe("value-c");
for (const testCase of cases) {
const cache = new DirectoryCache<string>(60_000, 2);
for (const action of testCase.actions) {
cache.set(action[1], action[2], cfg);
}
expect(cache.get("a", cfg)).toBe(testCase.expected.a);
expect(cache.get("b", cfg)).toBe(testCase.expected.b);
expect(cache.get("c", cfg)).toBe(testCase.expected.c);
}
});
});
@@ -470,103 +483,128 @@ describe("buildOutboundResultEnvelope", () => {
});
describe("formatOutboundDeliverySummary", () => {
it("falls back when result is missing", () => {
expect(formatOutboundDeliverySummary("telegram")).toBe(
"✅ Sent via Telegram. Message ID: unknown",
);
expect(formatOutboundDeliverySummary("imessage")).toBe(
"✅ Sent via iMessage. Message ID: unknown",
);
});
it("formats fallback and channel-specific detail variants", () => {
const cases = [
{
name: "fallback telegram",
channel: "telegram" as const,
result: undefined,
expected: "✅ Sent via Telegram. Message ID: unknown",
},
{
name: "fallback imessage",
channel: "imessage" as const,
result: undefined,
expected: "✅ Sent via iMessage. Message ID: unknown",
},
{
name: "telegram with chat detail",
channel: "telegram" as const,
result: {
channel: "telegram" as const,
messageId: "m1",
chatId: "c1",
},
expected: "✅ Sent via Telegram. Message ID: m1 (chat c1)",
},
{
name: "discord with channel detail",
channel: "discord" as const,
result: {
channel: "discord" as const,
messageId: "d1",
channelId: "chan",
},
expected: "✅ Sent via Discord. Message ID: d1 (channel chan)",
},
] as const;
it("adds chat or channel details", () => {
expect(
formatOutboundDeliverySummary("telegram", {
channel: "telegram",
messageId: "m1",
chatId: "c1",
}),
).toBe("✅ Sent via Telegram. Message ID: m1 (chat c1)");
expect(
formatOutboundDeliverySummary("discord", {
channel: "discord",
messageId: "d1",
channelId: "chan",
}),
).toBe("✅ Sent via Discord. Message ID: d1 (channel chan)");
for (const testCase of cases) {
expect(formatOutboundDeliverySummary(testCase.channel, testCase.result), testCase.name).toBe(
testCase.expected,
);
}
});
});
describe("buildOutboundDeliveryJson", () => {
it("builds direct delivery payloads", () => {
expect(
buildOutboundDeliveryJson({
channel: "telegram",
to: "123",
result: { channel: "telegram", messageId: "m1", chatId: "c1" },
mediaUrl: "https://example.com/a.png",
}),
).toEqual({
channel: "telegram",
via: "direct",
to: "123",
messageId: "m1",
mediaUrl: "https://example.com/a.png",
chatId: "c1",
});
});
it("builds direct delivery payloads across provider-specific fields", () => {
const cases = [
{
name: "telegram direct payload",
input: {
channel: "telegram" as const,
to: "123",
result: { channel: "telegram" as const, messageId: "m1", chatId: "c1" },
mediaUrl: "https://example.com/a.png",
},
expected: {
channel: "telegram",
via: "direct",
to: "123",
messageId: "m1",
mediaUrl: "https://example.com/a.png",
chatId: "c1",
},
},
{
name: "whatsapp metadata",
input: {
channel: "whatsapp" as const,
to: "+1",
result: { channel: "whatsapp" as const, messageId: "w1", toJid: "jid" },
},
expected: {
channel: "whatsapp",
via: "direct",
to: "+1",
messageId: "w1",
mediaUrl: null,
toJid: "jid",
},
},
{
name: "signal timestamp",
input: {
channel: "signal" as const,
to: "+1",
result: { channel: "signal" as const, messageId: "s1", timestamp: 123 },
},
expected: {
channel: "signal",
via: "direct",
to: "+1",
messageId: "s1",
mediaUrl: null,
timestamp: 123,
},
},
] as const;
it("supports whatsapp metadata when present", () => {
expect(
buildOutboundDeliveryJson({
channel: "whatsapp",
to: "+1",
result: { channel: "whatsapp", messageId: "w1", toJid: "jid" },
}),
).toEqual({
channel: "whatsapp",
via: "direct",
to: "+1",
messageId: "w1",
mediaUrl: null,
toJid: "jid",
});
});
it("keeps timestamp for signal", () => {
expect(
buildOutboundDeliveryJson({
channel: "signal",
to: "+1",
result: { channel: "signal", messageId: "s1", timestamp: 123 },
}),
).toEqual({
channel: "signal",
via: "direct",
to: "+1",
messageId: "s1",
mediaUrl: null,
timestamp: 123,
});
for (const testCase of cases) {
expect(buildOutboundDeliveryJson(testCase.input), testCase.name).toEqual(testCase.expected);
}
});
});
describe("formatGatewaySummary", () => {
it("formats gateway summaries with channel", () => {
expect(formatGatewaySummary({ channel: "whatsapp", messageId: "m1" })).toBe(
"✅ Sent via gateway (whatsapp). Message ID: m1",
);
});
it("formats default and custom gateway action summaries", () => {
const cases = [
{
name: "default send action",
input: { channel: "whatsapp", messageId: "m1" },
expected: "✅ Sent via gateway (whatsapp). Message ID: m1",
},
{
name: "custom action",
input: { action: "Poll sent", channel: "discord", messageId: "p1" },
expected: "✅ Poll sent via gateway (discord). Message ID: p1",
},
] as const;
it("supports custom actions", () => {
expect(
formatGatewaySummary({
action: "Poll sent",
channel: "discord",
messageId: "p1",
}),
).toBe("✅ Poll sent via gateway (discord). Message ID: p1");
for (const testCase of cases) {
expect(formatGatewaySummary(testCase.input), testCase.name).toBe(testCase.expected);
}
});
});
@@ -741,45 +779,50 @@ describe("resolveOutboundSessionRoute", () => {
});
describe("normalizeOutboundPayloadsForJson", () => {
it("normalizes payloads with mediaUrl and mediaUrls", () => {
expect(
normalizeOutboundPayloadsForJson([
{ text: "hi" },
{ text: "photo", mediaUrl: "https://x.test/a.jpg" },
{ text: "multi", mediaUrls: ["https://x.test/1.png"] },
]),
).toEqual([
{ text: "hi", mediaUrl: null, mediaUrls: undefined, channelData: undefined },
it("normalizes payloads for JSON output", () => {
const cases = [
{
text: "photo",
mediaUrl: "https://x.test/a.jpg",
mediaUrls: ["https://x.test/a.jpg"],
channelData: undefined,
input: [
{ text: "hi" },
{ text: "photo", mediaUrl: "https://x.test/a.jpg" },
{ text: "multi", mediaUrls: ["https://x.test/1.png"] },
],
expected: [
{ text: "hi", mediaUrl: null, mediaUrls: undefined, channelData: undefined },
{
text: "photo",
mediaUrl: "https://x.test/a.jpg",
mediaUrls: ["https://x.test/a.jpg"],
channelData: undefined,
},
{
text: "multi",
mediaUrl: null,
mediaUrls: ["https://x.test/1.png"],
channelData: undefined,
},
],
},
{
text: "multi",
mediaUrl: null,
mediaUrls: ["https://x.test/1.png"],
channelData: undefined,
input: [
{
text: "MEDIA:https://x.test/a.png\nMEDIA:https://x.test/b.png",
},
],
expected: [
{
text: "",
mediaUrl: null,
mediaUrls: ["https://x.test/a.png", "https://x.test/b.png"],
channelData: undefined,
},
],
},
]);
});
] as const;
it("keeps mediaUrl null for multi MEDIA tags", () => {
expect(
normalizeOutboundPayloadsForJson([
{
text: "MEDIA:https://x.test/a.png\nMEDIA:https://x.test/b.png",
},
]),
).toEqual([
{
text: "",
mediaUrl: null,
mediaUrls: ["https://x.test/a.png", "https://x.test/b.png"],
channelData: undefined,
},
]);
for (const testCase of cases) {
expect(normalizeOutboundPayloadsForJson(testCase.input)).toEqual(testCase.expected);
}
});
});
@@ -792,22 +835,29 @@ describe("normalizeOutboundPayloads", () => {
});
describe("formatOutboundPayloadLog", () => {
it("trims trailing text and appends media lines", () => {
expect(
formatOutboundPayloadLog({
text: "hello ",
mediaUrls: ["https://x.test/a.png", "https://x.test/b.png"],
}),
).toBe("hello\nMEDIA:https://x.test/a.png\nMEDIA:https://x.test/b.png");
});
it("formats text+media and media-only logs", () => {
const cases = [
{
name: "text with media lines",
input: {
text: "hello ",
mediaUrls: ["https://x.test/a.png", "https://x.test/b.png"],
},
expected: "hello\nMEDIA:https://x.test/a.png\nMEDIA:https://x.test/b.png",
},
{
name: "media only",
input: {
text: "",
mediaUrls: ["https://x.test/a.png"],
},
expected: "MEDIA:https://x.test/a.png",
},
] as const;
it("logs media-only payloads", () => {
expect(
formatOutboundPayloadLog({
text: "",
mediaUrls: ["https://x.test/a.png"],
}),
).toBe("MEDIA:https://x.test/a.png");
for (const testCase of cases) {
expect(formatOutboundPayloadLog(testCase.input), testCase.name).toBe(testCase.expected);
}
});
});
@@ -825,22 +875,6 @@ describe("resolveOutboundTarget", () => {
setActivePluginRegistry(createTestRegistry());
});
it("rejects whatsapp with empty target even when allowFrom configured", () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { allowFrom: ["+1555"] } },
};
const res = resolveOutboundTarget({
channel: "whatsapp",
to: "",
cfg,
mode: "explicit",
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("WhatsApp");
}
});
it.each([
{
name: "normalizes whatsapp target when provided",
@@ -860,6 +894,16 @@ describe("resolveOutboundTarget", () => {
},
expected: { ok: true as const, to: "120363401234567890@g.us" },
},
{
name: "rejects whatsapp with empty target in explicit mode even with cfg allowFrom",
input: {
channel: "whatsapp" as const,
to: "",
cfg: { channels: { whatsapp: { allowFrom: ["+1555"] } } } as OpenClawConfig,
mode: "explicit" as const,
},
expectedErrorIncludes: "WhatsApp",
},
{
name: "rejects whatsapp with empty target and allowFrom (no silent fallback)",
input: { channel: "whatsapp" as const, to: "", allowFrom: ["+1555"] },
@@ -901,19 +945,18 @@ describe("resolveOutboundTarget", () => {
}
});
it("rejects telegram with missing target", () => {
const res = resolveOutboundTarget({ channel: "telegram", to: " " });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("Telegram");
}
});
it("rejects invalid non-whatsapp targets", () => {
const cases = [
{ input: { channel: "telegram" as const, to: " " }, expectedErrorIncludes: "Telegram" },
{ input: { channel: "webchat" as const, to: "x" }, expectedErrorIncludes: "WebChat" },
] as const;
it("rejects webchat delivery", () => {
const res = resolveOutboundTarget({ channel: "webchat", to: "x" });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("WebChat");
for (const testCase of cases) {
const res = resolveOutboundTarget(testCase.input);
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain(testCase.expectedErrorIncludes);
}
}
});
});

View File

@@ -164,7 +164,7 @@ describe("fetchAntigravityUsage", () => {
project: { id: "projects/beta" },
expectedBody: JSON.stringify({ project: "projects/beta" }),
},
])("$name", async ({ project, expectedBody }) => {
])("project payload: $name", async ({ project, expectedBody }) => {
let capturedBody: string | undefined;
const mockFetch = createEndpointFetch({
loadCodeAssist: () =>
@@ -228,7 +228,7 @@ describe("fetchAntigravityUsage", () => {
},
expectedPlan: "Basic Plan",
},
])("$name", async ({ loadCodeAssist, expectedPlan }) => {
])("plan label: $name", async ({ loadCodeAssist, expectedPlan }) => {
const mockFetch = createEndpointFetch({
loadCodeAssist: () => makeResponse(200, loadCodeAssist),
fetchAvailableModels: () => makeResponse(500, "Error"),