Archived
mail 模块骨架:契约 / 端口 / 领域错误 / 游标两形 / 到期与租约(ADR-013)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/** 游标两形 —— domain 零 mock。IMAP 按 UID 逐封推进,wecom 按时间窗逐批推进 */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
advanceImap,
|
||||
cursorSummary,
|
||||
imapCursor,
|
||||
imapNeedsReanchor,
|
||||
newUids,
|
||||
parseCursor,
|
||||
wecomCursor,
|
||||
wecomWindow,
|
||||
WECOM_OVERLAP_SEC,
|
||||
} from '../internal/domain/cursor.ts';
|
||||
|
||||
describe('imap 游标', () => {
|
||||
it('逐封推进只往前不往后', () => {
|
||||
const c = imapCursor('7', 100);
|
||||
expect(advanceImap(c, 105).lastUid).toBe(105);
|
||||
expect(advanceImap(c, 90).lastUid).toBe(100);
|
||||
});
|
||||
|
||||
it('UIDVALIDITY 变了、或根本不是 imap 游标,都要重锚', () => {
|
||||
expect(imapNeedsReanchor(imapCursor('7', 100), '7')).toBe(false);
|
||||
expect(imapNeedsReanchor(imapCursor('7', 100), '8')).toBe(true);
|
||||
expect(imapNeedsReanchor(null, '7')).toBe(true);
|
||||
expect(imapNeedsReanchor(wecomCursor(1), '7')).toBe(true);
|
||||
});
|
||||
|
||||
it('**n:* 陷阱**:服务器会把最大那一封也回回来,客户端再按 uid > lastUid 过滤一遍,并升序去重', () => {
|
||||
expect(newUids([100, 103, 101, 103, 99], 100)).toEqual([101, 103]);
|
||||
expect(newUids([100], 100)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wecom 游标', () => {
|
||||
const now = new Date('2026-08-25T10:00:00Z');
|
||||
it('有游标:窗口从 lastTime 往前退十分钟(治时钟偏,重复由唯一键挡)到当下', () => {
|
||||
const w = wecomWindow(wecomCursor(1_000_000), null, now);
|
||||
expect(w).toEqual({ beginTime: 1_000_000 - WECOM_OVERLAP_SEC, endTime: Math.floor(now.getTime() / 1000) });
|
||||
});
|
||||
it('无游标且不回填:窗口空(begin = end),等锚定;设了回填就从那天起', () => {
|
||||
const end = Math.floor(now.getTime() / 1000);
|
||||
expect(wecomWindow(null, null, now)).toEqual({ beginTime: end, endTime: end });
|
||||
const since = new Date('2026-08-20T00:00:00Z');
|
||||
expect(wecomWindow(null, since, now).beginTime).toBe(Math.floor(since.getTime() / 1000));
|
||||
});
|
||||
it('lastTime 往前退不会退成负数', () => {
|
||||
expect(wecomWindow(wecomCursor(10), null, now).beginTime).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('摘要与解析', () => {
|
||||
it('人读摘要;没拉过是 null', () => {
|
||||
expect(cursorSummary(null)).toBeNull();
|
||||
expect(cursorSummary(imapCursor('7', 100))).toBe('UID 100 · validity 7');
|
||||
// brief 原字面量 1756116000 对应 2025-08-25T10:00:00.000Z(与期望的 2026 年份相差整 365 天,
|
||||
// 与本文件其余用例统一锚定的 2026-08-25T10:00:00Z 不符)——已核实为 brief 的数据笔误,
|
||||
// 修的是输入字面量而非断言期望值,见 task-4-report.md。
|
||||
expect(cursorSummary(wecomCursor(1787652000))).toBe('至 2026-08-25T10:00:00.000Z');
|
||||
});
|
||||
it('从 jsonb 解回来:形状不对当没有游标(重锚),不抛', () => {
|
||||
expect(parseCursor({ kind: 'imap', uidValidity: '7', lastUid: 3 })).toEqual(imapCursor('7', 3));
|
||||
expect(parseCursor({ kind: 'wecom', lastTime: 5 })).toEqual(wecomCursor(5));
|
||||
expect(parseCursor({ kind: 'imap', lastUid: 'x' })).toBeNull();
|
||||
expect(parseCursor(null)).toBeNull();
|
||||
expect(parseCursor('garbage')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/** 到期与租约 —— 从没拉过就到期;租约在手不算到期;租约时长下限 5 分钟 */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isDue, isPolling, leaseMsFor, MIN_LEASE_MS } from '../internal/domain/due.ts';
|
||||
|
||||
const now = new Date('2026-08-25T10:00:00Z');
|
||||
const base = { enabled: true, lastPolledAt: null, pollIntervalSec: 30, pollingUntil: null };
|
||||
|
||||
describe('isDue', () => {
|
||||
it('从没拉过 → 到期;停用 → 永不到期', () => {
|
||||
expect(isDue(base, now)).toBe(true);
|
||||
expect(isDue({ ...base, enabled: false }, now)).toBe(false);
|
||||
});
|
||||
it('上次 + 间隔 <= 现在才到期', () => {
|
||||
expect(isDue({ ...base, lastPolledAt: new Date(now.getTime() - 29_000) }, now)).toBe(false);
|
||||
expect(isDue({ ...base, lastPolledAt: new Date(now.getTime() - 30_000) }, now)).toBe(true);
|
||||
});
|
||||
it('租约还没过期 → 别人在拉,不到期;过期了照常', () => {
|
||||
expect(isDue({ ...base, pollingUntil: new Date(now.getTime() + 1) }, now)).toBe(false);
|
||||
expect(isDue({ ...base, pollingUntil: new Date(now.getTime() - 1) }, now)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaseMsFor / isPolling', () => {
|
||||
it('租约 = max(5 分钟, 3 × 间隔)', () => {
|
||||
expect(leaseMsFor(30)).toBe(MIN_LEASE_MS);
|
||||
expect(leaseMsFor(600)).toBe(1_800_000);
|
||||
});
|
||||
it('polling 只在租约未过期时为真', () => {
|
||||
expect(isPolling(null, now)).toBe(false);
|
||||
expect(isPolling(new Date(now.getTime() + 10), now)).toBe(true);
|
||||
expect(isPolling(new Date(now.getTime() - 10), now)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user