import test from "node:test"; import assert from "node:assert/strict"; import { updateTask, enableTaskRecovery } 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; } return null; }, async run() { const normalized = sql.trim(); if (/^UPDATE tasks\s+SET recovery_enabled = 1/.test(normalized)) { const [recovery_due_at, recovery_instructions, recovery_example_images, id] = values; const row = tasks.find((item) => item.id === id); if (!row) return { meta: { changes: 0 } }; Object.assign(row, { recovery_enabled: 1, recovery_due_at, recovery_instructions, recovery_example_images, }); return { meta: { changes: 1 } }; } if (/^UPDATE tasks/.test(normalized)) { const [ name, brand, due_at, project_id, platform, content_format, quantity, recovery_enabled, recovery_due_at, recovery_instructions, recovery_example_images, 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, recovery_enabled, recovery_due_at, recovery_instructions, recovery_example_images, }); return { meta: { changes: 1 } }; } 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 publishTask(overrides = {}) { return { id: "task-pub-1", task_type: "content_publish", quantity: 18, claimed_quantity: 2, platform: "抖音", content_format: "video", name: "旧名称", brand: "旧品牌", due_at: "2026-08-01", project_id: "", status: "active", recovery_enabled: 1, recovery_due_at: "2026-08-10", recovery_instructions: "请提交发布后的笔记截图", recovery_example_images: "[]", ...overrides, }; } function publishContents(quantity) { return Array.from({ length: quantity }, (_, index) => ({ id: `content-${index + 1}`, task_id: "task-pub-1", title: "笔记标题", body: "笔记正文", image_assets: "[]", status: index < 2 ? "claimed" : "available", source_row: index + 1, })); } test("updateTask rewrites basic fields, quantity and contents stay untouched", async () => { const { db, tasks, contents } = fakeDb({ task: publishTask(), contents: publishContents(5), }); const result = await updateTask( { taskId: "task-pub-1", name: "新名称", brand: "新品牌", dueAt: "2026-09-30", projectId: "proj-1", }, db, ); assert.equal(result.quantity, 18); assert.equal(result.claimedQuantity, 2); 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(tasks[0].quantity, 18, "quantity 由飞书行数决定,不可编辑"); assert.equal(contents.length, 5, "contents 不随编辑变动"); assert.equal(contents[0].title, "笔记标题"); }); test("updateTask keeps platform/contentFormat when not provided", async () => { const { db, tasks } = fakeDb({ task: publishTask() }); await updateTask( { taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-09-30" }, db, ); assert.equal(tasks[0].platform, "抖音"); assert.equal(tasks[0].content_format, "video"); }); test("updateTask applies provided platform/contentFormat", async () => { const { db, tasks } = fakeDb({ task: publishTask({ 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 updates recovery fields and can toggle recovery off", async () => { const { db, tasks } = fakeDb({ task: publishTask() }); await updateTask( { taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-09-30", recoveryEnabled: false, recoveryDueAt: "2026-10-15", recoveryInstructions: "新回收说明", }, db, ); assert.equal(tasks[0].recovery_enabled, 0); assert.equal(tasks[0].recovery_due_at, "2026-10-15"); assert.equal(tasks[0].recovery_instructions, "新回收说明"); }); test("updateTask keeps row recovery values when input omits them", async () => { const { db, tasks } = fakeDb({ task: publishTask({ recovery_enabled: 0, recovery_due_at: "", recovery_instructions: "", }), }); await updateTask( { taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-09-30" }, db, ); assert.equal(tasks[0].recovery_enabled, 0, "未传开关时保持关闭"); assert.equal( tasks[0].recovery_due_at, "2026-09-30", "回收截止为空时兜底为发布截止", ); }); test("updateTask replaces example image JSON when key provided", async () => { const { db, tasks } = fakeDb({ task: publishTask() }); await updateTask( { taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-09-30", recoveryExampleImageKey: "task-assets/new.png", }, db, ); assert.deepEqual(JSON.parse(tasks[0].recovery_example_images), [ { index: 1, key: "task-assets/new.png", width: null, height: null }, ]); }); test("enableTaskRecovery 开通老任务回收:默认截止=发布截止、默认说明、保留示例图", async () => { const { db, tasks } = fakeDb({ task: publishTask({ recovery_enabled: 0, recovery_due_at: "", recovery_instructions: "", recovery_example_images: "[]", }), }); const result = await enableTaskRecovery({ taskId: "task-pub-1" }, db); assert.equal(result.recoveryDueAt, "2026-08-01"); assert.equal(tasks[0].recovery_enabled, 1); assert.equal(tasks[0].recovery_due_at, "2026-08-01"); assert.equal(tasks[0].recovery_instructions, "请提交发布后的笔记截图"); const keepDb = fakeDb({ task: publishTask({ recovery_example_images: '[{"index":1,"key":"task-assets/keep.png","width":null,"height":null}]', }), }); await enableTaskRecovery( { taskId: "task-pub-1", recoveryDueAt: "2026-10-01", recoveryInstructions: "定制说明" }, keepDb.db, ); assert.equal(keepDb.tasks[0].recovery_due_at, "2026-10-01"); assert.equal(keepDb.tasks[0].recovery_instructions, "定制说明"); assert.deepEqual(JSON.parse(keepDb.tasks[0].recovery_example_images), [ { index: 1, key: "task-assets/keep.png", width: null, height: null }, ]); }); test("updateTask / enableTaskRecovery reject unknown tasks and invalid dates", async () => { const { db } = fakeDb({ task: publishTask() }); await assert.rejects( () => updateTask({ taskId: "missing", name: "n", brand: "b", dueAt: "2026-09-30" }, db), /任务不存在/, ); await assert.rejects( () => updateTask({ taskId: "task-pub-1", name: "n", brand: "b", dueAt: "2026-13-01" }, db), /截止日期/, ); await assert.rejects( () => enableTaskRecovery({ taskId: "missing" }, db), /任务不存在/, ); });