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/); });