import assert from "node:assert/strict"; import test from "node:test"; import { buildTaskGroupPushMessage, groupChatsByOwner, pushTaskToGroupChats, trimToBytes, } from "../lib/wecom-group-push-service.ts"; import { clearWecomAccessTokenCacheForTests, resolveWecomConfig, WecomClientError, } from "../lib/wecom-client.ts"; const baseBindings = { WECOM_CORP_ID: "corp-test", WECOM_AGENT_ID: "1000001", WECOM_SECRET: "secret-test", }; const baseTask = { id: "task-1", name: "测试任务", brand: "品牌A", quantity: 3, due_at: "2026-09-01", task_type: "content_publish", platform: "小红书", content_format: "image_text", share_token: "tok-123", }; test.beforeEach(() => { clearWecomAccessTokenCacheForTests(); }); test("trimToBytes cuts on UTF-8 boundaries", () => { assert.equal(trimToBytes("abc", 10), "abc"); assert.equal(trimToBytes("abcdef", 3), "abc"); // 「汉」占 3 字节 assert.equal(trimToBytes("a汉b", 4), "a汉"); assert.equal(trimToBytes("汉汉汉", 7), "汉汉"); }); test("buildTaskGroupPushMessage composes text and link with claim url", () => { const message = buildTaskGroupPushMessage(baseTask, "https://portal.example.com/"); assert.match(message.text, /【新任务】测试任务/); assert.match(message.text, /品牌:品牌A|数量:3 份|截止:2026-09-01/); assert.match(message.text, /平台:小红书|形式:图文/); assert.match(message.text, /领取链接:https:\/\/portal\.example\.com\/\?task=tok-123/); assert.ok(message.link); assert.equal(message.link.title, "测试任务"); assert.equal(message.link.url, "https://portal.example.com/?task=tok-123"); assert.match(message.link.desc, /品牌A · 3 份 · 截止 2026-09-01/); }); test("buildTaskGroupPushMessage marks video and screenshot tasks", () => { const video = buildTaskGroupPushMessage( { ...baseTask, content_format: "video" }, "https://portal.example.com", ); assert.match(video.text, /形式:视频/); const screenshot = buildTaskGroupPushMessage( { ...baseTask, task_type: "screenshot_collect" }, "https://portal.example.com", ); assert.match(screenshot.text, /形式:截图回收/); }); test("buildTaskGroupPushMessage drops link when share_token missing", () => { const message = buildTaskGroupPushMessage( { ...baseTask, share_token: null }, "https://portal.example.com", ); assert.equal(message.link, null); assert.match(message.text, /领取链接生成失败/); }); test("buildTaskGroupPushMessage trims long content to byte limits", () => { const longName = "长".repeat(200); const longBrand = "牌".repeat(400); const message = buildTaskGroupPushMessage( { ...baseTask, name: longName, brand: longBrand }, "https://portal.example.com", ); assert.ok(Buffer.byteLength(message.text, "utf8") <= 4000); assert.ok(Buffer.byteLength(message.link.title, "utf8") <= 128); assert.ok(Buffer.byteLength(message.link.desc, "utf8") <= 512); }); test("groupChatsByOwner groups chat ids by owner and skips empty owners", () => { const grouped = groupChatsByOwner([ { chat_id: "wr-1", name: "一", owner_user_id: "zhangsan", member_count: 1, status: 0 }, { chat_id: "wr-2", name: "二", owner_user_id: "lisi", member_count: 2, status: 0 }, { chat_id: "wr-3", name: "三", owner_user_id: "zhangsan", member_count: 3, status: 0 }, { chat_id: "wr-4", name: "四", owner_user_id: " ", member_count: 4, status: 0 }, ]); assert.deepEqual(grouped, [ { ownerUserId: "zhangsan", chatIds: ["wr-1", "wr-3"] }, { ownerUserId: "lisi", chatIds: ["wr-2"] }, ]); }); function fakeGroupPushDb(taskRow) { const executed = []; const groupRows = [ { chat_id: "wr-1", name: "一群", owner_user_id: "zhangsan", member_count: 10, status: 0 }, { chat_id: "wr-2", name: "二群", owner_user_id: "lisi", member_count: 5, status: 0 }, ]; const makeStatement = (sql, params = []) => ({ sql, params, bind(...args) { return makeStatement(sql, args); }, async run() { executed.push({ sql, params }); return {}; }, async first() { executed.push({ sql, params }); if (/FROM tasks WHERE id = \?/.test(sql)) { return params[0] === taskRow.id ? taskRow : null; } return null; }, async all() { executed.push({ sql, params }); if (/FROM wecom_group_chats/.test(sql)) { return { results: sql.includes(" IN (") ? groupRows.filter((row) => params.includes(row.chat_id)) : groupRows, }; } return { results: [] }; }, }); return { executed, prepare(sql) { return makeStatement(sql); }, async batch(statements) { for (const statement of statements) await statement.run(); }, }; } function fakeTemplateFetch(calls) { return async (input, init = {}) => { const url = new URL(String(input)); calls.push({ url, init }); if (url.pathname.endsWith("/cgi-bin/gettoken")) { return Response.json({ errcode: 0, errmsg: "ok", access_token: "token-test", expires_in: 7200, }); } if (url.pathname.endsWith("/cgi-bin/externalcontact/add_msg_template")) { return Response.json({ errcode: 0, errmsg: "ok", msgid: "msg-1", fail_list: [] }); } return new Response("not found", { status: 404 }); }; } test("pushTaskToGroupChats batches per owner and records each send", async () => { process.env.KOC_PORTAL_URL = "https://portal.example.com"; try { const db = fakeGroupPushDb(baseTask); const calls = []; const summary = await pushTaskToGroupChats( db, { taskId: "task-1", chatIds: ["wr-2", "wr-1"] }, resolveWecomConfig(baseBindings), fakeTemplateFetch(calls), ); assert.equal(summary.results.length, 2); const senders = summary.results.map((item) => item.sender).sort(); assert.deepEqual(senders, ["lisi", "zhangsan"]); const templateCalls = calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/externalcontact/add_msg_template"), ); assert.equal(templateCalls.length, 2, "one call per owner"); const bodies = templateCalls.map((c) => JSON.parse(String(c.init.body))); const bySender = new Map(bodies.map((body) => [body.sender, body])); assert.deepEqual(bySender.get("zhangsan").chat_id_list, ["wr-1"]); assert.deepEqual(bySender.get("lisi").chat_id_list, ["wr-2"]); assert.match(bySender.get("zhangsan").text.content, /【新任务】测试任务/); assert.equal( bySender.get("zhangsan").attachments[0].link.url, "https://portal.example.com/?task=tok-123", ); const inserts = db.executed.filter((item) => item.sql.includes("INSERT INTO wecom_group_pushes"), ); assert.equal(inserts.length, 2); } finally { delete process.env.KOC_PORTAL_URL; } }); test("pushTaskToGroupChats rejects unknown chat ids", async () => { const db = fakeGroupPushDb(baseTask); const calls = []; await assert.rejects( () => pushTaskToGroupChats( db, { taskId: "task-1", chatIds: ["wr-404"] }, resolveWecomConfig(baseBindings), fakeTemplateFetch(calls), ), (error) => { assert.ok(error instanceof WecomClientError); assert.equal(error.status, 400); assert.match(error.message, /未同步/); return true; }, ); }); test("pushTaskToGroupChats rejects missing task", async () => { const db = fakeGroupPushDb(baseTask); await assert.rejects( () => pushTaskToGroupChats( db, { taskId: "task-404", chatIds: ["wr-1"] }, resolveWecomConfig(baseBindings), fakeTemplateFetch([]), ), (error) => { assert.ok(error instanceof WecomClientError); assert.equal(error.status, 404); return true; }, ); }); test("pushTaskToGroupChats prefers explicit text over template", async () => { process.env.KOC_PORTAL_URL = "https://portal.example.com"; try { const db = fakeGroupPushDb(baseTask); const calls = []; await pushTaskToGroupChats( db, { taskId: "task-1", chatIds: ["wr-1"], text: "运营手动编辑的文案" }, resolveWecomConfig(baseBindings), fakeTemplateFetch(calls), ); const templateCall = calls.find((c) => c.url.pathname.endsWith("/cgi-bin/externalcontact/add_msg_template"), ); const body = JSON.parse(String(templateCall.init.body)); assert.equal(body.text.content, "运营手动编辑的文案"); } finally { delete process.env.KOC_PORTAL_URL; } });