import test from "node:test"; import assert from "node:assert/strict"; import { updateTask } from "../lib/task-service.ts"; function fakeDb({ task, contents = [] }) { const tasks = task ? [task] : []; const rows = contents.map((row) => ({ ...row })); const db = { prepare(sql) { return { bind(...values) { return { async first() { if (/FROM tasks WHERE id = \?/.test(sql)) { return tasks.find((row) => row.id === values[0]) ?? null; } if (/SELECT image_assets FROM contents/.test(sql)) { const sorted = rows .filter((row) => row.task_id === values[0]) .sort((a, b) => a.source_row - b.source_row); return sorted[0] ? { image_assets: sorted[0].image_assets } : null; } return null; }, async run() { const normalized = sql.trim(); if (/^UPDATE tasks/.test(normalized)) { const [name, brand, due_at, project_id, platform, content_format, quantity, id] = values; const row = tasks.find((item) => item.id === id); if (!row) return { meta: { changes: 0 } }; Object.assign(row, { name, brand, due_at, project_id, platform, content_format, quantity, }); return { meta: { changes: 1 } }; } if (/^INSERT INTO contents/.test(normalized)) { const [id, task_id, title, body, image_assets, source_row] = values; rows.push({ id, task_id, title, body, image_assets, status: "available", source_row, }); return { meta: { changes: 1 } }; } if (/^DELETE FROM contents/.test(normalized)) { const [taskId, limit] = values; const available = rows .filter((row) => row.task_id === taskId && row.status === "available") .sort((a, b) => b.source_row - a.source_row); const doomed = new Set(available.slice(0, limit).map((row) => row.id)); for (let index = rows.length - 1; index >= 0; index -= 1) { if (doomed.has(rows[index].id)) rows.splice(index, 1); } return { meta: { changes: doomed.size } }; } if (/^UPDATE contents SET title/.test(normalized)) { const [title, body, taskId] = values; for (const row of rows) { if (row.task_id === taskId) { row.title = title; row.body = body; } } return { meta: { changes: rows.length } }; } if (/^UPDATE contents SET image_assets/.test(normalized)) { const [image_assets, taskId] = values; for (const row of rows) { if (row.task_id === taskId) row.image_assets = image_assets; } return { meta: { changes: rows.length } }; } return { meta: { changes: 0 } }; }, async all() { return { results: [] }; }, }; }, }; }, async batch(statements) { const results = []; for (const statement of statements) { results.push(await statement.run()); } return results; }, }; return { db, tasks, contents: rows }; } function screenshotTask(overrides = {}) { return { id: "task-shot-1", task_type: "screenshot_collect", quantity: 3, claimed_quantity: 0, platform: "小红书", content_format: "image_text", name: "旧名称", brand: "旧品牌", due_at: "2026-08-01", project_id: "", ...overrides, }; } function screenshotContents(quantity, overrides = {}) { return Array.from({ length: quantity }, (_, index) => ({ id: `content-${index + 1}`, task_id: "task-shot-1", title: overrides.title ?? "旧关键词", body: overrides.body ?? "旧说明", image_assets: overrides.imageAssets ?? "[]", status: index < (overrides.claimed ?? 0) ? "claimed" : "available", source_row: index + 1, })); } test("updateTask rewrites basic fields and syncs keyword/instructions on all rows", async () => { const { db, tasks, contents } = fakeDb({ task: screenshotTask(), contents: screenshotContents(3), }); const result = await updateTask( { taskId: "task-shot-1", name: "新名称", brand: "新品牌", dueAt: "2026-09-30", projectId: "proj-1", keyword: "新关键词", instructions: "新说明", quantity: 3, }, db, ); assert.equal(result.quantity, 3); assert.equal(tasks[0].name, "新名称"); assert.equal(tasks[0].brand, "新品牌"); assert.equal(tasks[0].due_at, "2026-09-30"); assert.equal(tasks[0].project_id, "proj-1"); assert.equal(contents.length, 3); for (const row of contents) { assert.equal(row.title, "新关键词"); assert.equal(row.body, "新说明"); } }); test("updateTask grows placeholder contents when quantity increases", async () => { const imageAssets = JSON.stringify([{ index: 1, key: "task-assets/a.png", width: null, height: null }]); const { db, tasks, contents } = fakeDb({ task: screenshotTask(), contents: screenshotContents(3, { imageAssets }), }); await updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: "k", instructions: "i", quantity: 5, }, db, ); assert.equal(tasks[0].quantity, 5); assert.equal(contents.length, 5); const added = contents.filter((row) => row.source_row > 3); assert.equal(added.length, 2); for (const row of added) { assert.equal(row.status, "available"); assert.equal(row.image_assets, imageAssets, "new rows copy example image"); assert.equal(row.title, "k"); } }); test("updateTask shrinks only available tail rows when quantity decreases", async () => { const { db, tasks, contents } = fakeDb({ task: screenshotTask({ quantity: 5, claimed_quantity: 2 }), contents: screenshotContents(5, { claimed: 2 }), }); await updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: "k", instructions: "i", quantity: 3, }, db, ); assert.equal(tasks[0].quantity, 3); assert.equal(contents.length, 3); const claimedRows = contents.filter((row) => row.status === "claimed"); assert.equal(claimedRows.length, 2, "claimed rows preserved"); assert.ok( contents.every((row) => row.source_row <= 3), "tail rows removed", ); }); test("updateTask rejects quantity below claimed_quantity", async () => { const { db } = fakeDb({ task: screenshotTask({ quantity: 5, claimed_quantity: 2 }), contents: screenshotContents(5, { claimed: 2 }), }); await assert.rejects( () => updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: "k", instructions: "i", quantity: 1, }, db, ), /不能低于已领取的 2 份/, ); }); test("updateTask validates screenshot quantity and keyword", async () => { const { db } = fakeDb({ task: screenshotTask(), contents: screenshotContents(3) }); await assert.rejects( () => updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: "k", instructions: "i", quantity: 0 }, db, ), /1—500/, ); await assert.rejects( () => updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: " ", instructions: "i", quantity: 3 }, db, ), /请补全搜索关键词和任务说明/, ); }); test("updateTask replaces example image across contents when re-uploaded", async () => { const { db, contents } = fakeDb({ task: screenshotTask(), contents: screenshotContents(3), }); await updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-09-30", keyword: "k", instructions: "i", quantity: 3, exampleImageKey: "task-assets/new.png", }, db, ); for (const row of contents) { assert.equal(JSON.parse(row.image_assets)[0].key, "task-assets/new.png"); } }); test("updateTask ignores quantity and keyword for content_publish tasks", async () => { const { db, tasks, contents } = fakeDb({ task: screenshotTask({ id: "task-pub-1", task_type: "content_publish", quantity: 18, platform: "抖音", content_format: "video", }), contents: [ { id: "content-pub-1", task_id: "task-pub-1", title: "笔记标题", body: "笔记正文", image_assets: "[]", status: "available", source_row: 1, }, ], }); await updateTask( { taskId: "task-pub-1", name: "新名称", brand: "新品牌", dueAt: "2026-09-30", projectId: "proj-2", quantity: 99, keyword: "ignored", instructions: "ignored", }, db, ); assert.equal(tasks[0].quantity, 18, "feishu row count untouched"); assert.equal(tasks[0].platform, "抖音", "platform kept when not provided"); assert.equal(tasks[0].content_format, "video"); assert.equal(contents[0].title, "笔记标题", "contents untouched"); }); test("updateTask applies provided platform/contentFormat for content_publish", async () => { const { db, tasks } = fakeDb({ task: screenshotTask({ id: "task-pub-1", task_type: "content_publish", platform: "小红书", content_format: "image_text", }), }); await updateTask( { taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-09-30", platform: "抖音", contentFormat: "video", }, db, ); assert.equal(tasks[0].platform, "抖音"); assert.equal(tasks[0].content_format, "video"); }); test("updateTask rejects unknown tasks and invalid dates", async () => { const { db } = fakeDb({ task: screenshotTask() }); await assert.rejects( () => updateTask( { taskId: "missing", name: "n", brand: "b", dueAt: "2026-09-30" }, db, ), /任务不存在/, ); await assert.rejects( () => updateTask( { taskId: "task-shot-1", name: "n", brand: "b", dueAt: "2026-13-01" }, db, ), /截止日期/, ); });