From 47b0ee36ff8aed305d48feb9a62e0980649f6468 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 19:27:04 +0000 Subject: [PATCH] test: expand message action helper coverage --- .../outbound/message-action-params.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/infra/outbound/message-action-params.test.ts b/src/infra/outbound/message-action-params.test.ts index 996db9682..f72bd2d26 100644 --- a/src/infra/outbound/message-action-params.test.ts +++ b/src/infra/outbound/message-action-params.test.ts @@ -2,15 +2,145 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { hydrateAttachmentParamsForAction, + normalizeSandboxMediaList, normalizeSandboxMediaParams, + resolveAttachmentMediaPolicy, + resolveSlackAutoThreadId, + resolveTelegramAutoThreadId, } from "./message-action-params.js"; const cfg = {} as OpenClawConfig; const maybeIt = process.platform === "win32" ? it.skip : it; +function createToolContext( + overrides: Partial = {}, +): ChannelThreadingToolContext { + return { + currentChannelId: "C123", + currentThreadTs: "thread-1", + replyToMode: "all", + ...overrides, + }; +} + +describe("message action threading helpers", () => { + it("resolves Slack auto-thread ids only for matching active channels", () => { + expect( + resolveSlackAutoThreadId({ + to: "#c123", + toolContext: createToolContext(), + }), + ).toBe("thread-1"); + expect( + resolveSlackAutoThreadId({ + to: "channel:C999", + toolContext: createToolContext(), + }), + ).toBeUndefined(); + expect( + resolveSlackAutoThreadId({ + to: "user:U123", + toolContext: createToolContext(), + }), + ).toBeUndefined(); + }); + + it("skips Slack auto-thread ids when reply mode or context blocks them", () => { + expect( + resolveSlackAutoThreadId({ + to: "C123", + toolContext: createToolContext({ + replyToMode: "first", + hasRepliedRef: { value: true }, + }), + }), + ).toBeUndefined(); + expect( + resolveSlackAutoThreadId({ + to: "C123", + toolContext: createToolContext({ replyToMode: "off" }), + }), + ).toBeUndefined(); + expect( + resolveSlackAutoThreadId({ + to: "C123", + toolContext: createToolContext({ currentThreadTs: undefined }), + }), + ).toBeUndefined(); + }); + + it("resolves Telegram auto-thread ids for matching chats across target formats", () => { + expect( + resolveTelegramAutoThreadId({ + to: "telegram:group:-100123:topic:77", + toolContext: createToolContext({ + currentChannelId: "tg:group:-100123", + }), + }), + ).toBe("thread-1"); + expect( + resolveTelegramAutoThreadId({ + to: "-100999:77", + toolContext: createToolContext({ + currentChannelId: "-100123", + }), + }), + ).toBeUndefined(); + expect( + resolveTelegramAutoThreadId({ + to: "-100123", + toolContext: createToolContext({ currentChannelId: undefined }), + }), + ).toBeUndefined(); + }); +}); + +describe("message action media helpers", () => { + it("prefers sandbox media policy when sandbox roots are non-blank", () => { + expect( + resolveAttachmentMediaPolicy({ + sandboxRoot: " /tmp/workspace ", + mediaLocalRoots: ["/tmp/a"], + }), + ).toEqual({ + mode: "sandbox", + sandboxRoot: "/tmp/workspace", + }); + expect( + resolveAttachmentMediaPolicy({ + sandboxRoot: " ", + mediaLocalRoots: ["/tmp/a"], + }), + ).toEqual({ + mode: "host", + localRoots: ["/tmp/a"], + }); + }); + + maybeIt("normalizes sandbox media lists and dedupes resolved workspace paths", async () => { + const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-list-")); + try { + await expect( + normalizeSandboxMediaList({ + values: [" data:text/plain;base64,QQ== "], + }), + ).rejects.toThrow(/data:/i); + await expect( + normalizeSandboxMediaList({ + values: [" file:///workspace/assets/photo.png ", "/workspace/assets/photo.png", " "], + sandboxRoot: ` ${sandboxRoot} `, + }), + ).resolves.toEqual([path.join(sandboxRoot, "assets", "photo.png")]); + } finally { + await fs.rm(sandboxRoot, { recursive: true, force: true }); + } + }); +}); + describe("message action sandbox media hydration", () => { maybeIt("rejects symlink retarget escapes after sandbox media normalization", async () => { const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-sandbox-"));