Files
koc-loop/tests/claimed-quantity.test.mjs
ABAPPLO 2db06063ee fix: 采集失败不降级有数据分行+任务领取计数对齐实际分发行数
- 采集失败时按 latest_likes 分流:无数据才标 failed,已有历史数据保持
  success 并注明沿用上次数据,collection_updated_at 不动;run 行照实记失败
- 种子 seedIfEmpty 末尾统一对账 claimed_quantity,杜绝演示数据口径漂移
- 新增 mysql/0021 对账迁移,修复 0020 物理合并遗留的生产计数漂移
- 运营端展示全面改读实时 dist_total(任务卡双面板/项目树/概览/进度),
  project-service 查询补 dist_total 子查询
- 走查实证:卡片 7/18 vs 详情已领取 2 两处口径打架;保存采集计划补采
  失败会把历史成功行改写为 failed
2026-10-08 14:30:36 +08:00

54 lines
2.4 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";
// claimed_quantity 口径:领取(+1)/释放(-1) 维护的不变式是
// claimed_quantity == 当前分发行数。种子与 0020 迁移都可能造成漂移,
// 这里钉住三道防线:种子末尾对账、0021 生产对账、UI 一律读实时 dist_total。
test("seed reconciles claimed_quantity with actual distribution rows", async () => {
const source = await readFile(
new URL("../lib/mvp-db.ts", import.meta.url),
"utf8",
);
const reconcile = source.indexOf(
"SET claimed_quantity = (\n SELECT COUNT(*) FROM distributions d WHERE d.task_id = tasks.id",
);
assert.ok(reconcile > 0, "种子阶段缺少 claimed_quantity 对账 UPDATE");
const lastDistributionInsert = source.lastIndexOf(
"INSERT INTO distributions",
);
assert.ok(
reconcile > lastDistributionInsert,
"对账必须发生在所有分发 INSERT 之后",
);
const batchEnd = source.indexOf("await db.batch(batch);", reconcile);
assert.ok(batchEnd > reconcile, "对账语句必须在 seedIfEmpty 的 batch 内执行");
});
test("migration 0021 recalculates drifted claimed_quantity in production", async () => {
const migration = await readFile(
new URL("../mysql/0021_reconcile_claimed_quantity.sql", import.meta.url),
"utf8",
);
assert.match(migration, /UPDATE tasks\s+SET claimed_quantity = \(\s+SELECT COUNT\(\*\) FROM distributions d\s+WHERE d\.task_id = tasks\.id\s+\)/);
assert.match(migration, /WHERE claimed_quantity <> \(/);
});
test("admin surfaces read the live distribution count, not the counter", async () => {
const [adminApp, projectService] = await Promise.all([
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
readFile(new URL("../lib/project-service.ts", import.meta.url), "utf8"),
]);
// 任务卡双面板 + 概览统计
assert.match(adminApp, /panelMetric\(task\.dist_total \?\? 0, task\.quantity, "已领取"/);
assert.match(adminApp, /panelMetric\(task\.dist_submitted \?\? 0, task\.dist_total \?\? 0, "已收取"/);
assert.match(adminApp, /sum \+ \(task\.dist_total \?\? 0\)/);
// 项目树明细同样用实时口径
assert.match(projectService, /AS dist_total/);
assert.match(adminApp, /\{task\.dist_total\} \/ \{task\.quantity\}/);
// 展示层不再读反规范化计数器
assert.doesNotMatch(adminApp, /task\.claimed_quantity/);
});