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