feat: 项目总群自动建群+任务负责人后台视角+客户群活码进群+任务编辑
- 项目总群:≥3 人 appchat/create 自动建群(chatid=pgc+projectId),成员 diff 增删同步;DROP project_group_chats;推送改 appchat/send 群发、无群降级逐人 - 任务负责人:tasks.owner_userid(新建/编辑任务通讯录选人),task_set_owner 分配即预建 tgc 任务群发介绍,群主链接只归属负责人 - 任务编辑 task_update:基础字段+截图任务数量/关键词/说明,contents 调平只删 available 尾行 - 删除「一键推送」:task-one-click-push-service 整体移除(recordCardAck 移入 wecom-callback-events) - 负责人后台视角:users.wecom_userid 绑定,lib/owner-scope 行级过滤(undefined=全量/""=空范围/非空=过滤),~30 action 补 managerGate/taskOwnerGate,owner 登录仅见工作台/任务中心/数据回收/复盘报表 - 任务绑定客户群+进群活码:tasks 增 customer_chat_id/name、join_config_id、join_qr_code 4 列,join_way add/update/get/del(换绑走 update 保 config_id 活码不变),后台「客户群」弹窗,portal 认领后扫码自助进群(满员自动开新群) - 迁移 0013/0014/0015;催办支持按任务;企微可信域名 IP 验证文件 - 288 测试全绿;真机全链路验收通过(列群/活码四端点/portal 渲染/解绑清空)
This commit is contained in:
219
tests/task-customer-chat-service.test.mjs
Normal file
219
tests/task-customer-chat-service.test.mjs
Normal file
@@ -0,0 +1,219 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
bindTaskCustomerChat,
|
||||
listOwnerCustomerChats,
|
||||
unbindTaskCustomerChat,
|
||||
} from "../lib/task-customer-chat-service.ts";
|
||||
import { resolveWecomConfig } from "../lib/wecom-client.ts";
|
||||
|
||||
const baseBindings = {
|
||||
WECOM_CORP_ID: "corp-test",
|
||||
WECOM_AGENT_ID: "1000001",
|
||||
WECOM_SECRET: "secret-test",
|
||||
};
|
||||
|
||||
function fakeWecom(options = {}) {
|
||||
const calls = [];
|
||||
const fetchImpl = 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/groupchat/add_join_way")) {
|
||||
if (options.addErrcode) {
|
||||
return Response.json({ errcode: options.addErrcode, errmsg: "add failed" });
|
||||
}
|
||||
return Response.json({ errcode: 0, errmsg: "ok", config_id: options.configId ?? "cfg-new" });
|
||||
}
|
||||
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/update_join_way")) {
|
||||
if (options.updateErrcode) {
|
||||
return Response.json({ errcode: options.updateErrcode, errmsg: "update failed" });
|
||||
}
|
||||
return Response.json({ errcode: 0, errmsg: "ok" });
|
||||
}
|
||||
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/get_join_way")) {
|
||||
if (options.getErrcode) {
|
||||
return Response.json({ errcode: options.getErrcode, errmsg: "get failed" });
|
||||
}
|
||||
return Response.json({
|
||||
errcode: 0,
|
||||
errmsg: "ok",
|
||||
join_way: { qr_code: options.qrCode ?? "https://p.qpic.cn/qr-live.png" },
|
||||
});
|
||||
}
|
||||
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/del_join_way")) {
|
||||
if (options.delErrcode) {
|
||||
return Response.json({ errcode: options.delErrcode, errmsg: "del failed" });
|
||||
}
|
||||
return Response.json({ errcode: 0, errmsg: "ok" });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
return { calls, fetchImpl };
|
||||
}
|
||||
|
||||
function captureDb(taskRow) {
|
||||
const captured = [];
|
||||
const makeStatement = (sql, values) => ({
|
||||
async first() {
|
||||
captured.push({ sql, values });
|
||||
if (/FROM tasks WHERE id = \?/.test(sql)) return taskRow ?? null;
|
||||
return null;
|
||||
},
|
||||
async run() {
|
||||
captured.push({ sql, values });
|
||||
return { changes: 1 };
|
||||
},
|
||||
});
|
||||
const db = {
|
||||
prepare(sql) {
|
||||
const unbound = makeStatement(sql, []);
|
||||
return {
|
||||
bind(...values) {
|
||||
return makeStatement(sql, values);
|
||||
},
|
||||
first: unbound.first,
|
||||
run: unbound.run,
|
||||
};
|
||||
},
|
||||
};
|
||||
return { db, captured };
|
||||
}
|
||||
|
||||
function callsTo(calls, suffix) {
|
||||
return calls.filter((c) => c.url.pathname.endsWith(suffix));
|
||||
}
|
||||
|
||||
test("listOwnerCustomerChats rejects without app credentials and ignores empty owner", async () => {
|
||||
const config = resolveWecomConfig({ ...baseBindings, WECOM_SECRET: "" });
|
||||
await assert.rejects(() => listOwnerCustomerChats(config, "zhangsan"), /未配置/);
|
||||
const { calls, fetchImpl } = fakeWecom();
|
||||
assert.deepEqual(await listOwnerCustomerChats(resolveWecomConfig(baseBindings), " ", fetchImpl), []);
|
||||
assert.equal(calls.length, 0, "empty owner short-circuits without API calls");
|
||||
});
|
||||
|
||||
test("bindTaskCustomerChat creates join way when task has no config", async () => {
|
||||
const { db, captured } = captureDb({
|
||||
id: "task-1",
|
||||
name: "美团探店任务",
|
||||
join_config_id: null,
|
||||
});
|
||||
const { calls, fetchImpl } = fakeWecom();
|
||||
const result = await bindTaskCustomerChat(
|
||||
db,
|
||||
{ taskId: "task-1", chatId: "wr-normal-1", chatName: "KOC 一群" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fetchImpl,
|
||||
);
|
||||
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 1);
|
||||
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 0);
|
||||
const addBody = JSON.parse(String(callsTo(calls, "/add_join_way")[0].init.body));
|
||||
assert.deepEqual(addBody.chat_id_list, ["wr-normal-1"]);
|
||||
assert.equal(addBody.room_base_name, "美团探店任务");
|
||||
assert.equal(addBody.state, "task-task-1");
|
||||
assert.equal(result.joinConfigId, "cfg-new");
|
||||
assert.equal(result.joinQrCode, "https://p.qpic.cn/qr-live.png");
|
||||
assert.equal(result.qrError, undefined);
|
||||
const update = captured.find((c) => /UPDATE tasks/.test(c.sql));
|
||||
assert.ok(update, "tasks row updated");
|
||||
assert.match(update.sql, /customer_chat_id = \?/);
|
||||
assert.deepEqual(update.values, ["wr-normal-1", "KOC 一群", "cfg-new", "https://p.qpic.cn/qr-live.png", "task-1"]);
|
||||
});
|
||||
|
||||
test("bindTaskCustomerChat reuses update_join_way on rebind and falls back to add", async () => {
|
||||
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: "cfg-old" });
|
||||
const { calls, fetchImpl } = fakeWecom();
|
||||
const result = await bindTaskCustomerChat(
|
||||
db,
|
||||
{ taskId: "task-1", chatId: "wr-normal-2", chatName: "KOC 二群" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fetchImpl,
|
||||
);
|
||||
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 1);
|
||||
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 0);
|
||||
assert.equal(result.joinConfigId, "cfg-old", "config id preserved so qr stays valid");
|
||||
|
||||
const { db: db2 } = captureDb({ id: "task-1", name: "任务", join_config_id: "cfg-old" });
|
||||
const { calls: calls2, fetchImpl: fetch2 } = fakeWecom({
|
||||
updateErrcode: 40068,
|
||||
configId: "cfg-rebuilt",
|
||||
});
|
||||
const result2 = await bindTaskCustomerChat(
|
||||
db2,
|
||||
{ taskId: "task-1", chatId: "wr-normal-2", chatName: "KOC 二群" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fetch2,
|
||||
);
|
||||
assert.equal(callsTo(calls2, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 1);
|
||||
assert.equal(callsTo(calls2, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 1);
|
||||
assert.equal(result2.joinConfigId, "cfg-rebuilt");
|
||||
});
|
||||
|
||||
test("bindTaskCustomerChat tolerates qr fetch failure and records qrError", async () => {
|
||||
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: null });
|
||||
const { fetchImpl } = fakeWecom({ getErrcode: 40068 });
|
||||
const result = await bindTaskCustomerChat(
|
||||
db,
|
||||
{ taskId: "task-1", chatId: "wr-normal-1", chatName: "KOC 一群" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fetchImpl,
|
||||
);
|
||||
assert.equal(result.joinQrCode, "");
|
||||
assert.match(result.qrError, /活码二维码获取失败/);
|
||||
});
|
||||
|
||||
test("bindTaskCustomerChat validates missing task / chat", async () => {
|
||||
const { db: missingTask } = captureDb(null);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
bindTaskCustomerChat(
|
||||
missingTask,
|
||||
{ taskId: "task-x", chatId: "wr-1", chatName: "群" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fakeWecom().fetchImpl,
|
||||
),
|
||||
/任务不存在/,
|
||||
);
|
||||
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: null });
|
||||
await assert.rejects(
|
||||
() =>
|
||||
bindTaskCustomerChat(
|
||||
db,
|
||||
{ taskId: "task-1", chatId: "", chatName: "" },
|
||||
resolveWecomConfig(baseBindings),
|
||||
fakeWecom().fetchImpl,
|
||||
),
|
||||
/请选择要绑定的客户群/,
|
||||
);
|
||||
});
|
||||
|
||||
test("unbindTaskCustomerChat deletes join way (tolerating failure) and clears columns", async () => {
|
||||
const { db, captured } = captureDb({ id: "task-1", join_config_id: "cfg-old" });
|
||||
const { calls, fetchImpl } = fakeWecom();
|
||||
await unbindTaskCustomerChat(db, "task-1", resolveWecomConfig(baseBindings), fetchImpl);
|
||||
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/del_join_way").length, 1);
|
||||
const update = captured.find((c) => /UPDATE tasks/.test(c.sql));
|
||||
assert.ok(update);
|
||||
assert.match(update.sql, /customer_chat_id = ''/);
|
||||
|
||||
const { db: db2, captured: captured2 } = captureDb({ id: "task-1", join_config_id: "cfg-old" });
|
||||
const { fetchImpl: fetch2 } = fakeWecom({ delErrcode: 40068 });
|
||||
await unbindTaskCustomerChat(db2, "task-1", resolveWecomConfig(baseBindings), fetch2);
|
||||
assert.ok(
|
||||
captured2.some((c) => /UPDATE tasks/.test(c.sql)),
|
||||
"local columns still cleared when del_join_way fails",
|
||||
);
|
||||
|
||||
const { db: db3, captured: captured3 } = captureDb({ id: "task-1", join_config_id: "" });
|
||||
const { calls: calls3, fetchImpl: fetch3 } = fakeWecom();
|
||||
await unbindTaskCustomerChat(db3, "task-1", resolveWecomConfig(baseBindings), fetch3);
|
||||
assert.equal(calls3.length, 0, "no wecom calls when nothing bound");
|
||||
assert.ok(captured3.some((c) => /UPDATE tasks/.test(c.sql)));
|
||||
});
|
||||
Reference in New Issue
Block a user