Files
koc-loop/tests/task-report.test.mjs
ABAPPLO 8f1ebf7697 feat: 负责人下钻视图+项目负责人编辑管理+企微授权自助绑定
- 任务/项目负责人 union scope:任务中心分组视图、工作台项目进度卡、收录验证列
- 项目负责人可管理自己项目:编辑/删除项目、编辑任务、分配负责人、推送、项目内建任务;堵死转移归属/越权新建/任务迁移
- 只读依赖放开给已登录:inspect_feishu、wecom_list_internal_contacts
- 企微 OAuth 自助绑定:/api/wecom-bind(state 签名校验+登录态一致性+归属冲突检查),未绑定 banner 增加入口
- 工作台项目进度卡与未绑定 banner 排版修复
- 修 user-auth 判空分支被 Turbopack dev 折叠删除导致匿名请求 500
2026-09-17 15:21:24 +08:00

193 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { buildTaskReportCsv } from "../lib/task-report-csv.ts";
import { getTaskReportData } from "../lib/mvp-db.ts";
const reportRow = {
id: "task-A",
name: "夏日种草计划",
task_type: "content_publish",
brand: "轻食品牌",
platform: "小红书",
status: "active",
due_at: "2026-08-20",
created_at: "2026-08-01 10:00:00",
quantity: 20,
project_name: "八月投放",
claimed_count: 10,
published_count: 8,
completed_count: 5,
exposure_total: 12000,
views_total: 9000,
d2_likes: 100,
d2_comments: 20,
d2_collects: 30,
d5_likes: 150,
d5_comments: 25,
d5_collects: 40,
d7_likes: 180,
d7_comments: 30,
d7_collects: 50,
latest_likes: 200,
latest_comments: 40,
latest_collects: 60,
latest_shares: 10,
related_task_id: "",
related_task_name: null,
reverse_related_task_id: null,
reverse_related_task_name: null,
submitted_count: 0,
};
test("buildTaskReportCsv writes BOM, headers and mapped rows", () => {
const csv = buildTaskReportCsv([reportRow]);
assert.ok(csv.startsWith(""), "starts with UTF-8 BOM");
const lines = csv.slice(1).split("\r\n");
assert.equal(lines.length, 2);
assert.equal(
lines[0],
[
"任务ID",
"任务名称",
"任务类型",
"品牌",
"平台",
"所属项目",
"任务状态",
"计划数量",
"领取数",
"发布数",
"完成数",
"完成率",
"截止日期",
"创建时间",
"曝光量合计",
"阅读量合计",
"D2点赞",
"D2评论",
"D2收藏",
"D5点赞",
"D5评论",
"D5收藏",
"D7点赞",
"D7评论",
"D7收藏",
"最新点赞",
"最新评论",
"最新收藏",
"最新转发",
"关联任务",
"截图提交数",
].join(","),
);
assert.equal(
lines[1],
[
"task-A",
"夏日种草计划",
"内容发布",
"轻食品牌",
"小红书",
"八月投放",
"进行中",
20,
10,
8,
5,
"50%",
"2026-08-20",
"2026-08-01 10:00:00",
12000,
9000,
100,
20,
30,
150,
25,
40,
180,
30,
50,
200,
40,
60,
10,
"",
"",
].join(","),
);
});
test("buildTaskReportCsv escapes commas and quotes, blanks null metrics", () => {
const csv = buildTaskReportCsv([
{
...reportRow,
name: '含,逗号"引号"任务',
project_name: null,
claimed_count: 0,
exposure_total: null,
views_total: null,
d2_likes: null,
d2_comments: null,
d2_collects: null,
d5_likes: null,
d5_comments: null,
d5_collects: null,
d7_likes: null,
d7_comments: null,
d7_collects: null,
latest_likes: null,
latest_comments: null,
latest_collects: null,
latest_shares: null,
},
]);
const rowLine = csv.slice(1).split("\r\n")[1];
assert.ok(rowLine.includes('"含,逗号""引号""任务"'));
const cells = rowLine.split(",");
assert.equal(cells.at(-1), "", "content publish row leaves screenshot count empty");
assert.equal(cells.at(-2), "", "no related task leaves empty");
assert.equal(cells.at(-20), "", "no claims means empty completion rate");
assert.equal(cells.at(-18), "2026-08-01 10:00:00");
});
test("buildTaskReportCsv writes related batch and screenshot submission for collect tasks", () => {
const csv = buildTaskReportCsv([
{
...reportRow,
task_type: "screenshot_collect",
related_task_id: "task-B",
related_task_name: "夏日种草计划",
submitted_count: 7,
claimed_count: 12,
},
]);
const cells = csv.slice(1).split("\r\n")[1].split(",");
assert.equal(cells.at(-2), "夏日种草计划");
assert.equal(cells.at(-1), "7");
});
test("getTaskReportData aggregates per task via distributions join", async () => {
let capturedSql = "";
const db = {
prepare(sql) {
capturedSql = sql;
return {
async all() {
return { results: [reportRow] };
},
};
},
};
const rows = await getTaskReportData(db);
assert.deepEqual(rows, [reportRow]);
assert.match(capturedSql, /FROM tasks t/);
assert.match(capturedSql, /LEFT JOIN distributions d ON d\.task_id = t\.id/);
assert.match(capturedSql, /LEFT JOIN projects p ON p\.id = t\.project_id/);
assert.match(capturedSql, /GROUP BY t\.id/);
assert.match(capturedSql, /publish_url/);
assert.match(capturedSql, /latest_shares/);
assert.match(capturedSql, /related_task_id/);
assert.match(capturedSql, /result_submitted_at/);
});