backend-ts/src/logic-bug-review-2026-11-11.spec.ts
Rama main · réplica de lectura del repositorio original.
/**
* 2026-11-11 逻辑 BUG 审查——BUG-1 回归用例
* (详见 docs/code-review/2026-11-11-logic-bug-review-01.md)。
*
* 原为 it.fails 存在性证明;`legacyMode: false` 落地后已转正为常规断言。
* 另保留单方受限 expression 的回归,防止 AND 语义误伤常见的
* `0 0 10 ? * MON-FRI` 形态。
*
* 原 BUG-5(channelAssistantReply 空正文不回落兜底)经复核回退:既有
* citation-markers.spec.ts 已固化「content 为 null → 空串」契约,改兜底属产品取舍,
* 不按 logic bug 修复;对应存在性用例已移除。
*/
import { describe, expect, it } from 'vitest';
import { buildCronPreview } from './schedule/scheduled-task.service.js';
// ---------------------------------------------------------------------------
// BUG-1(高)DOM+DOW 受限的 Spring cron 按 legacyMode 的 OR 语义触发
// 位置:backend-ts/src/schedule/scheduled-task.service.ts 全部 5 处
// `new Cron(..., { ...CRON_OPTIONS })`;CRON_OPTIONS 含 timezone + legacyMode: false
// 契约:产品接受的 cron 方言是 Spring/Quartz(normalizeSpringCron 专门把 Spring 的
// `?` 归一成 croner 的 `*`)。Spring 语义下 DOM 与 DOW **同时受限**时取交集
// (两者都要满足);croner 默认 legacyMode=true 是 POSIX 的 OR 语义
// (任一命中即触发)。
// 实测(node 直探):'0 0 12 1 1 1'(1 月 1 日且周一,Spring 下上次触发是
// 2024-01-01、下次是 2029-01-01)在修复前 nextRuns 从 2027-01-01(周五)
// 开始、并把 2027 年 1 月的每个周一全部列出——每周都会跑,与用户意图(四年
// 一次)严重偏离。
// 修复:5 处 Cron 构造统一使用 CRON_OPTIONS(legacyMode: false,croner 的 Quartz 语义)。
// ---------------------------------------------------------------------------
describe('BUG-1: DOM+DOW 受限的 Spring cron 按 OR 语义触发(应为 AND)', () => {
it('0 0 12 1 1 1 的每次触发都必须是「1 月 1 日且周一」', () => {
const preview = buildCronPreview('0 0 12 1 1 1', 5);
expect(preview.valid).toBe(true);
expect(preview.nextFireTimes.length).toBeGreaterThan(0);
for (const fireTime of preview.nextFireTimes) {
// AND 语义下每次触发同时满足:日期为 01-01,且星期一
expect(fireTime.slice(5, 10), `触发点 ${fireTime} 不是 1 月 1 日`).toBe('01-01');
const [y, m, d] = fireTime.split(' ')[0]!.split('-').map(Number);
const dow = new Date(Date.UTC(y, m - 1, d)).getUTCDay();
expect(dow, `触发点 ${fireTime} 不是周一(POSIX OR 语义把每个周一都排进来了)`).toBe(1);
}
});
it('单方受限的 ? * MON-FRI 仍按周一至周五触发(AND 不误伤通配字段)', () => {
const preview = buildCronPreview('0 0 10 ? * MON-FRI', 5);
expect(preview.valid).toBe(true);
expect(preview.nextFireTimes).toHaveLength(5);
for (const fireTime of preview.nextFireTimes) {
const [y, m, d] = fireTime.split(' ')[0]!.split('-').map(Number);
const dow = new Date(Date.UTC(y, m - 1, d)).getUTCDay();
expect(dow, `触发点 ${fireTime} 落在周末`).toBeGreaterThanOrEqual(1);
expect(dow, `触发点 ${fireTime} 落在周末`).toBeLessThanOrEqual(5);
}
});
});