mirror of
https://github.com/wangwangit/SubsTracker.git
synced 2026-07-02 00:54:25 +08:00
新增三个 KV 仓库(v3 通知/调度可观测性数据底座):
1. src/data/reminders.repo.js
- reminder_rules:{subId} 数组,支持 CRUD + 智能预设(4 条:到期前 7/3/1/当天)
- legacyFieldToRule 把 v2 的 reminderUnit/reminderValue 单点提醒
转成 1 条等价 before_expiry 规则
- normalizeRule 修复非法字段,repeatInterval 仅 after_expiry 类型保留
2. src/data/notification-logs.repo.js
- notify_log:{ymdh}:{subId}:{ruleId}:{channel}:{rand}, TTL 30 天
- writeLog / query(subId/channel/status/since/until/limit 过滤)
- 时间倒序,KV.list 拉全后排序
3. src/data/scheduler-logs.repo.js
- sched_log:{isoUtc}, TTL 30 天
- writeLog / getRecent(按时间倒序)
迁移扩展:MIGRATION_STEPS 追加两个 step
- reminder_rules_v3:为现有订阅生成默认提醒规则
- scheduler_logs_v3:v2 的 scheduler_status_history 转换到新表
调试页新增链接:/debug?export=sched_logs&limit=50 直接下载 JSON
测试:
- tests/data/reminders.test.js (13)
- tests/data/notification-logs.test.js (8)
- tests/data/scheduler-logs.test.js (4)
- 共 85 条测试全绿;lint 干净
Refs Task 4 of refactor/v3-product-grade plan.
101 lines
3.7 KiB
JavaScript
101 lines
3.7 KiB
JavaScript
// @ts-check
|
||
/**
|
||
* 提醒规则仓库单元测试
|
||
*/
|
||
import { describe, it, expect, beforeEach } from 'vitest';
|
||
// @ts-ignore
|
||
import { env } from 'cloudflare:test';
|
||
|
||
import * as repo from '../../src/data/reminders.repo.js';
|
||
|
||
async function clearKv() {
|
||
const list = await env.SUBSCRIPTIONS_KV.list();
|
||
await Promise.all(list.keys.map((k) => env.SUBSCRIPTIONS_KV.delete(k.name)));
|
||
}
|
||
|
||
beforeEach(clearKv);
|
||
|
||
describe('reminders.repo', () => {
|
||
it('listForSubscription 空返回空数组', async () => {
|
||
expect(await repo.listForSubscription(env, 's1')).toEqual([]);
|
||
});
|
||
|
||
it('addRule + listForSubscription', async () => {
|
||
const r = await repo.addRule(env, 's1', { type: 'before_expiry', value: 7, unit: 'days' });
|
||
expect(r.id).toBeTruthy();
|
||
expect(r.value).toBe(7);
|
||
const list = await repo.listForSubscription(env, 's1');
|
||
expect(list).toHaveLength(1);
|
||
expect(list[0].id).toBe(r.id);
|
||
});
|
||
|
||
it('updateRule 合并 patch', async () => {
|
||
const r = await repo.addRule(env, 's1', { type: 'before_expiry', value: 7, unit: 'days' });
|
||
const upd = await repo.updateRule(env, 's1', r.id, { value: 3, isEnabled: false });
|
||
expect(upd?.value).toBe(3);
|
||
expect(upd?.isEnabled).toBe(false);
|
||
const list = await repo.listForSubscription(env, 's1');
|
||
expect(list[0].value).toBe(3);
|
||
});
|
||
|
||
it('deleteRule 不存在返回 false', async () => {
|
||
expect(await repo.deleteRule(env, 's1', 'nope')).toBe(false);
|
||
});
|
||
|
||
it('deleteRule 存在返回 true', async () => {
|
||
const r = await repo.addRule(env, 's1', { type: 'on_expiry', value: 0, unit: 'days' });
|
||
expect(await repo.deleteRule(env, 's1', r.id)).toBe(true);
|
||
expect(await repo.listForSubscription(env, 's1')).toEqual([]);
|
||
});
|
||
|
||
it('clearForSubscription 清空整个 key', async () => {
|
||
await repo.addRule(env, 's1', { type: 'before_expiry', value: 7, unit: 'days' });
|
||
await repo.clearForSubscription(env, 's1');
|
||
expect(await repo.listForSubscription(env, 's1')).toEqual([]);
|
||
});
|
||
|
||
it('defaultPresetRules 返回 4 条预设(7/3/1/当天)', () => {
|
||
const rules = repo.defaultPresetRules();
|
||
expect(rules).toHaveLength(4);
|
||
expect(rules.map((r) => `${r.type}:${r.value}${r.unit[0]}`)).toEqual([
|
||
'before_expiry:7d',
|
||
'before_expiry:3d',
|
||
'before_expiry:1d',
|
||
'on_expiry:0d'
|
||
]);
|
||
rules.forEach((r) => expect(r.isEnabled).toBe(true));
|
||
});
|
||
|
||
it('legacyFieldToRule:day 单位 7 → before_expiry/7/days', () => {
|
||
const r = repo.legacyFieldToRule({ reminderUnit: 'day', reminderValue: 7 });
|
||
expect(r).toMatchObject({ type: 'before_expiry', value: 7, unit: 'days' });
|
||
});
|
||
|
||
it('legacyFieldToRule:hour 单位', () => {
|
||
const r = repo.legacyFieldToRule({ reminderUnit: 'hour', reminderValue: 12 });
|
||
expect(r).toMatchObject({ type: 'before_expiry', value: 12, unit: 'hours' });
|
||
});
|
||
|
||
it('legacyFieldToRule:value=0 → on_expiry', () => {
|
||
const r = repo.legacyFieldToRule({ reminderUnit: 'day', reminderValue: 0 });
|
||
expect(r.type).toBe('on_expiry');
|
||
});
|
||
|
||
it('legacyFieldToRule:缺失值兜底 7 天', () => {
|
||
const r = repo.legacyFieldToRule({});
|
||
expect(r).toMatchObject({ type: 'before_expiry', value: 7, unit: 'days' });
|
||
});
|
||
|
||
it('normalizeRule:非法 type 兜底为 before_expiry', () => {
|
||
const r = repo.normalizeRule({ type: 'lol', value: 5, unit: 'days' });
|
||
expect(r.type).toBe('before_expiry');
|
||
});
|
||
|
||
it('normalizeRule:repeatInterval 仅 after_expiry 保留', () => {
|
||
const a = repo.normalizeRule({ type: 'before_expiry', value: 1, unit: 'days', repeatInterval: 24 });
|
||
expect(a.repeatInterval).toBeNull();
|
||
const b = repo.normalizeRule({ type: 'after_expiry', value: 0, unit: 'days', repeatInterval: 24 });
|
||
expect(b.repeatInterval).toBe(24);
|
||
});
|
||
});
|