Files
koc-loop/tests/collection-schedule.test.mjs
ABAPPLO 87f4d4066d feat: KOC详情弹窗 + 任务中心单入口改版 + 资源池默认合作方/采集兜底
KOC 详情弹窗(合作方/数据回收/工作台三处入口):
- 新增 KocDetailModal:基本信息 / 绑定KOC账号画像 / 参与任务记录 / 履约漏斗
- 工作台最近分发、回收队列账号名、截图回收合作方名、合作方页名称、分发详情头部合作方均可点开
- 分发表行点击仍打开分发详情(stopPropagation 隔离)

任务中心单入口改版(对齐"单任务双阶段"模型):
- 任务卡发布/回收面板整块可点,全卡仅保留一个「查看明细」,回收面板重复按钮移除
- 详情页自述身份:动态 label(任务详情·分发与回收一体/仅分发)+ 阶段徽章(含回收·截止/未开通回收)
- 内容分发表新增阶段筛选 chips(全部/待发布/已发布待数据/待收截图,未开通回收不显示截图项),筛选落空给专用空态文案
- 数据回收页入口文案改为「选择一个任务进入回收队列」,全站不再区分发布/回收两种任务类型

资源池 KOC 默认加入合作方 + 采集兜底:
- partners 新增 released_account_id(mysql/0023):显式解绑/被顶替的账号不再被默认合作方同步回填
- 任务未设置采集计划时,默认按发布时间起连续采集 7 天,等待文案显示「等待D1 · 默认7天」
2026-10-12 11:11:31 +08:00

260 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. 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 { readFile } from "node:fs/promises";
import test from "node:test";
import {
DEFAULT_COLLECTION_DAYS,
effectiveCollectionDays,
isCollectionScheduleDue,
parseCollectionDays,
runDueScheduledCollections,
} from "../lib/collection-service.ts";
import { sortWithNullsLast } from "../lib/sort-utils.ts";
test("runs the Beijing collection schedule from 09:00", () => {
const scheduledDate = "2026-08-12";
assert.equal(
isCollectionScheduleDue(
scheduledDate,
Date.parse("2026-08-12T00:59:59Z"),
),
false,
);
assert.equal(
isCollectionScheduleDue(
scheduledDate,
Date.parse("2026-08-12T01:00:00Z"),
),
true,
);
assert.equal(
isCollectionScheduleDue(
scheduledDate,
Date.parse("2026-08-13T00:00:00Z"),
),
true,
);
assert.equal(
isCollectionScheduleDue(
scheduledDate,
Date.parse("2026-08-11T16:00:00Z"),
),
false,
);
});
test("sorts both directions while keeping missing values last", () => {
const values = [
{ id: "missing-a", value: null },
{ id: "middle", value: 20 },
{ id: "high", value: 50 },
{ id: "missing-b", value: null },
{ id: "low", value: 10 },
];
assert.deepEqual(
sortWithNullsLast(values, (item) => item.value, "asc").map(
(item) => item.id,
),
["low", "middle", "high", "missing-a", "missing-b"],
);
assert.deepEqual(
sortWithNullsLast(values, (item) => item.value, "desc").map(
(item) => item.id,
),
["high", "middle", "low", "missing-a", "missing-b"],
);
});
test("a failed collection keeps historically successful rows on success", async () => {
const source = await readFile(
new URL("../lib/collection-service.ts", import.meta.url),
"utf8",
);
// 失败写入必须按 latest_likes 分流:无数据 → failed;
// 已有历史数据 → 保持 success,仅描述里说明沿用上次数据。
assert.match(
source,
/collection_status = CASE\s+WHEN latest_likes IS NULL THEN 'failed'\s+ELSE 'success'\s+END/,
);
assert.match(
source,
/ELSE \?\s+END,\s+collection_updated_at = CASE\s+WHEN latest_likes IS NULL THEN CURRENT_TIMESTAMP\s+ELSE collection_updated_at/,
);
assert.match(source, /`\$\{message\},沿用上次采集数据`/);
// run 行照实记录失败,保留可观测性
assert.match(source, /UPDATE collection_runs\s+SET status = 'failed'/);
});
// —— 未设置采集计划时默认连采 7 天 ——
test("effective collection days fall back to 7 days when unset", () => {
assert.deepEqual(effectiveCollectionDays(null), [1, 2, 3, 4, 5, 6, 7]);
assert.deepEqual(effectiveCollectionDays("[]"), DEFAULT_COLLECTION_DAYS);
assert.deepEqual(effectiveCollectionDays('{"bad":1}'), DEFAULT_COLLECTION_DAYS);
assert.deepEqual(effectiveCollectionDays("[0,9]"), DEFAULT_COLLECTION_DAYS);
assert.deepEqual(effectiveCollectionDays("[3,1,2,2]"), [1, 2, 3]);
assert.deepEqual(parseCollectionDays("[7,2]"), [2, 7]);
});
function fakeSchedulerDb({ tasks = [], published = [] } = {}) {
const executed = [];
const record = (sql, values) => executed.push({ sql, values });
const db = {
prepare(sql) {
return {
bind(...values) {
return {
async first() {
record(sql, values);
return null;
},
async run() {
record(sql, values);
return { meta: { changes: 0 } };
},
async all() {
record(sql, values);
if (/FROM tasks/.test(sql)) {
const scoped = tasks.filter(
(task) => !values[0] || task.id === values[0],
);
return {
results: scoped.filter(
(task) =>
(task.task_type ?? "content_publish") !==
"screenshot_collect",
),
};
}
if (/COALESCE\(d\.publish_time, d\.claimed_at\)/.test(sql)) {
return {
results: published.filter((row) => row.task_id === values[0]),
};
}
// 无待采集行,避免触发真实 MCP 采集
return { results: [] };
},
};
},
};
},
async batch() {
return [];
},
};
return { db, executed };
}
test("collects unplanned tasks on the default 7-day schedule from publish date", async () => {
const { db, executed } = fakeSchedulerDb({
tasks: [
{
id: "task-a",
task_type: "content_publish",
collection_start_date: null,
collection_days: "[]",
},
],
published: [
// UTC 2026-10-09 01:30 → 上海 2026-10-09,D1–D4 在 10-12 09:00 前到期
{ id: "dist-a", task_id: "task-a", publish_anchor: "2026-10-09 01:30:00" },
],
});
const summary = await runDueScheduledCollections(
db,
Date.parse("2026-10-12T01:00:00Z"),
{},
);
assert.equal(summary.dueTasks, 4);
const anchorQuery = executed.find((entry) =>
/COALESCE\(d\.publish_time/.test(entry.sql),
);
assert.ok(anchorQuery, "未设置计划时应查询每篇的发布时间");
assert.equal(anchorQuery.values[0], "task-a");
const scoped = executed.filter((entry) =>
/SELECT d\.id FROM distributions d/.test(entry.sql),
);
assert.equal(scoped.length, 4);
assert.ok(scoped.every((entry) => entry.values.includes("dist-a")));
});
test("skips the default window once 7 days have passed since publish", async () => {
const { db, executed } = fakeSchedulerDb({
tasks: [
{
id: "task-old",
task_type: "content_publish",
collection_start_date: null,
collection_days: "[]",
},
],
published: [
{ id: "dist-old", task_id: "task-old", publish_anchor: "2026-09-01 00:00:00" },
],
});
const summary = await runDueScheduledCollections(
db,
Date.parse("2026-10-12T01:00:00Z"),
{},
);
assert.equal(summary.dueTasks, 0);
assert.ok(
!executed.some((entry) =>
/SELECT d\.id FROM distributions d/.test(entry.sql),
),
);
});
test("keeps the explicit task-level schedule when a plan is saved", async () => {
const { db, executed } = fakeSchedulerDb({
tasks: [
{
id: "task-b",
task_type: "content_publish",
collection_start_date: "2026-10-10",
collection_days: "[2]",
},
],
});
const summary = await runDueScheduledCollections(
db,
Date.parse("2026-10-12T01:00:00Z"),
{},
);
assert.equal(summary.dueTasks, 1);
const needing = executed.filter((entry) =>
/SELECT d\.id FROM distributions d/.test(entry.sql),
);
assert.deepEqual(needing.map((entry) => entry.values), [
["task-b", "2026-10-11"],
]);
assert.ok(
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
);
});
test("ignores screenshot collection tasks in the scheduler", async () => {
const { db, executed } = fakeSchedulerDb({
tasks: [
{
id: "task-shot",
task_type: "screenshot_collect",
collection_start_date: null,
collection_days: "[]",
},
],
published: [
{ id: "dist-shot", task_id: "task-shot", publish_anchor: "2026-10-11 00:00:00" },
],
});
const summary = await runDueScheduledCollections(
db,
Date.parse("2026-10-12T01:00:00Z"),
{},
);
assert.equal(summary.dueTasks, 0);
assert.ok(
!executed.some((entry) => /COALESCE\(d\.publish_time/.test(entry.sql)),
);
});