feat: add release controls and 09:00 collection

This commit is contained in:
巫凤萍
2026-08-12 11:43:26 +08:00
parent 1f1887c860
commit 1766a90cc1
14 changed files with 665 additions and 38 deletions

View File

@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isCollectionScheduleDue } from "../lib/collection-schedule.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,
);
});
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"],
);
});

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import {
DistributionReleaseError,
distributionReleaseBlockReason,
} from "../lib/distribution-release-service.ts";
test("allows only unfinished assignments to return to the claim pool", () => {
assert.equal(
distributionReleaseBlockReason({ publishUrl: null, resultSubmittedAt: null }),
null,
);
assert.equal(
distributionReleaseBlockReason({
publishUrl: "https://www.xiaohongshu.com/discovery/item/example",
resultSubmittedAt: null,
}),
"已回填发布链接的笔记不能释放",
);
assert.equal(
distributionReleaseBlockReason({
publishUrl: null,
resultSubmittedAt: "2026-08-12 09:00:00",
}),
"已提交结果截图的任务不能释放",
);
assert.equal(new DistributionReleaseError("blocked", 409).status, 409);
});
test("releases a claim in one D1 batch and restores counters", async () => {
const [service, actionRoute, adminApp] = await Promise.all([
readFile(new URL("../lib/distribution-release-service.ts", import.meta.url), "utf8"),
readFile(new URL("../app/api/action/route.ts", import.meta.url), "utf8"),
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
]);
assert.match(service, /await db\.batch\(statements\)/);
assert.match(service, /DELETE FROM distributions WHERE id = \?/);
assert.match(service, /UPDATE contents SET status = 'available'/);
assert.match(service, /claimed_quantity = MAX\(claimed_quantity - 1, 0\)/);
assert.match(service, /claimed_total = MAX\(claimed_total - 1, 0\)/);
assert.match(service, /UPDATE claims/);
assert.match(service, /UPDATE delegation_bundles/);
assert.match(actionRoute, /body\.action === "release_distribution"/);
assert.match(actionRoute, /isManagerRequest/);
assert.match(adminApp, /确认释放这篇笔记/);
assert.match(adminApp, /已释放,可重新领取/);
assert.match(adminApp, /role="alertdialog"/);
});

View File

@@ -214,6 +214,10 @@ test("supports task collection schedules and latest public metrics", async () =>
}
assert.match(adminApp, /选择开始日期与采集日/);
assert.match(adminApp, /第1天到第7天/);
assert.match(adminApp, /SortableRecoveryHeader/);
assert.match(adminApp, /sortRecoveryDistributions/);
assert.match(adminApp, /aria-sort=/);
assert.match(adminApp, /direction === "asc" \? "↑" : "↓"/);
assert.match(adminApp, /内容 \/ 发布账号/);
assert.match(adminApp, /recovery-title-link/);
assert.match(adminApp, /打开小红书笔记/);
@@ -233,17 +237,18 @@ test("supports task collection schedules and latest public metrics", async () =>
assert.match(collectionService, /runDueScheduledCollections/);
assert.match(collectionService, /retryFailedCollections/);
assert.match(collectionService, /exposure IS NOT NULL AND views IS NOT NULL/);
assert.match(collectionService, /等待第\$\{scheduleDay\}天 10:00自动采集/);
assert.match(collectionService, /等待第\$\{scheduleDay\}天 09:00自动采集/);
assert.match(collectionService, /isCollectionScheduleDue/);
assert.doesNotMatch(collectionService, /latestDueSchedule/);
assert.match(collectionService, /自动追采/);
assert.match(worker, /async scheduled/);
assert.match(worker, /backfillAccountProfiles/);
assert.match(bootstrapRoute, /backfillAccountProfiles/);
assert.match(viteConfig, /"0 2 \* \* \*"/);
assert.match(viteConfig, /"0 1 \* \* \*"/);
assert.match(migration, /latest_likes/);
assert.match(migration, /collection_runs_distribution_date_idx/);
assert.match(accountMigration, /public_account_id/);
assert.match(deployConfig, /"crons":\["0 2 \* \* \*"\]/);
assert.match(deployConfig, /"crons":\["0 1 \* \* \*"\]/);
});
test("exports complete task recovery data to Excel with embedded images", async () => {