chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,281 @@
import { describe, it, expect } from 'vitest';
import {
startOfDay, endOfDay, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
isSameDay, addDays, addMonths, monthGrid, weekDays, remindAt,
bucketByDay, dayKey, renderReminder, toLocalInput, fromLocalInput,
describeRecurrence, describeRemindBefore
} from '../../src/lib/calendar';
import type { CalendarEvent } from '../../src/types';
let seq = 0;
function ev(over: Partial<CalendarEvent> = {}): CalendarEvent {
seq += 1;
return {
event_id: `e${String(seq).padStart(3, '0')}`,
title: `事件 ${seq}`,
description: '',
reminder_text: '',
agent_name: 'pi',
to_address: '',
recipients: [],
delivery_mode: 'separate',
event_time: '2026-09-03T14:30:00+08:00',
remind_before: 0,
recurrence: 'none',
status: 'active',
created_at: '2026-09-01T00:00:00Z',
updated_at: '2026-09-01T00:00:00Z',
created_by: 'jianf',
...over
};
}
describe('日期边界', () => {
it('startOfDay / endOfDay 用本地时区', () => {
const d = new Date(2026, 8, 3, 14, 30, 45, 123);
expect(startOfDay(d).getHours()).toBe(0);
expect(startOfDay(d).getMilliseconds()).toBe(0);
expect(endOfDay(d).getHours()).toBe(23);
expect(endOfDay(d).getMilliseconds()).toBe(999);
// 不修改入参
expect(d.getHours()).toBe(14);
});
it('startOfMonth / endOfMonth', () => {
const d = new Date(2026, 8, 15);
expect(startOfMonth(d).getDate()).toBe(1);
expect(endOfMonth(d).getDate()).toBe(30); // 9 月 30 天
expect(endOfMonth(new Date(2026, 1, 5)).getDate()).toBe(28); // 2026 年 2 月
});
it('周从周一开始,周日归到上一周末尾', () => {
// 2026-09-06 是周日
const sunday = new Date(2026, 8, 6);
expect(sunday.getDay()).toBe(0);
// 它所在周的周一应是 08-31不是 09-07
expect(dayKey(startOfWeek(sunday))).toBe('2026-08-31');
expect(dayKey(endOfWeek(sunday))).toBe('2026-09-06');
});
it('周一自己就是周首', () => {
const monday = new Date(2026, 8, 7);
expect(monday.getDay()).toBe(1);
expect(dayKey(startOfWeek(monday))).toBe('2026-09-07');
});
});
describe('日期运算', () => {
it('addDays 不修改入参', () => {
const d = new Date(2026, 8, 3);
const r = addDays(d, 5);
expect(dayKey(r)).toBe('2026-09-08');
expect(dayKey(d)).toBe('2026-09-03');
});
it('addDays 跨月', () => {
expect(dayKey(addDays(new Date(2026, 8, 29), 5))).toBe('2026-10-04');
});
it('addMonths 从月末加一个月不会溢出', () => {
// setMonth 在 1 月 31 日上加一个月会给 3 月 2/3 日
const jan31 = new Date(2026, 0, 31);
expect(dayKey(addMonths(jan31, 1))).toBe('2026-02-01');
});
it('isSameDay 只比年月日', () => {
expect(isSameDay(new Date(2026, 8, 3, 0, 0), new Date(2026, 8, 3, 23, 59))).toBe(true);
expect(isSameDay(new Date(2026, 8, 3), new Date(2026, 8, 4))).toBe(false);
expect(isSameDay(new Date(2026, 8, 3), new Date(2025, 8, 3))).toBe(false);
});
});
describe('monthGrid', () => {
it('固定 42 格6 行 × 7 列)', () => {
// 行数变化会让网格高度跳动,翻月时页面内容上下弹
expect(monthGrid(new Date(2026, 8, 15))).toHaveLength(42);
expect(monthGrid(new Date(2026, 1, 15))).toHaveLength(42);
});
it('首格是月首所在周的周一', () => {
// 2026-09-01 是周二 → 首格应是 08-31周一
const cells = monthGrid(new Date(2026, 8, 15));
expect(dayKey(cells[0])).toBe('2026-08-31');
});
it('格子连续无空洞', () => {
const cells = monthGrid(new Date(2026, 8, 15));
for (let i = 1; i < cells.length; i++) {
const diff = cells[i].getTime() - cells[i - 1].getTime();
// 允许夏令时造成的 ±1 小时偏差
expect(diff).toBeGreaterThanOrEqual(23 * 3600_000);
expect(diff).toBeLessThanOrEqual(25 * 3600_000);
}
});
it('包含整个当月', () => {
const anchor = new Date(2026, 8, 15);
const keys = monthGrid(anchor).map(dayKey);
expect(keys).toContain('2026-09-01');
expect(keys).toContain('2026-09-30');
});
});
describe('weekDays', () => {
it('给 7 天,周一起头', () => {
const days = weekDays(new Date(2026, 8, 3)); // 周四
expect(days).toHaveLength(7);
expect(dayKey(days[0])).toBe('2026-08-31');
expect(dayKey(days[6])).toBe('2026-09-06');
});
});
describe('remindAt', () => {
it('提前 30 分钟', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 30 });
expect(remindAt(e).toISOString()).toBe('2026-09-03T06:00:00.000Z');
});
it('remind_before 为 0 时就是事件时间', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 0 });
expect(remindAt(e).toISOString()).toBe('2026-09-03T06:30:00.000Z');
});
it('提前一整天', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 1440 });
expect(remindAt(e).toISOString()).toBe('2026-09-02T06:30:00.000Z');
});
});
describe('bucketByDay', () => {
it('按本地日期分桶,不用 UTC 前缀', () => {
// 东八区 22:00 → UTC 是前一天 14:00。用 toISOString().slice(0,10) 会归错天
const e = ev({ event_time: '2026-09-03T22:00:00+08:00' });
const m = bucketByDay([e]);
expect([...m.keys()]).toEqual(['2026-09-03']);
});
it('同一天内按时间正序', () => {
const late = ev({ event_time: '2026-09-03T18:00:00+08:00', title: '晚' });
const early = ev({ event_time: '2026-09-03T09:00:00+08:00', title: '早' });
const m = bucketByDay([late, early]);
expect(m.get('2026-09-03')!.map(x => x.title)).toEqual(['早', '晚']);
});
it('同刻事件用 event_id 兜底定序', () => {
const ts = '2026-09-03T09:00:00+08:00';
const a = ev({ event_id: 'aaa', event_time: ts });
const z = ev({ event_id: 'zzz', event_time: ts });
const m1 = bucketByDay([a, z]).get('2026-09-03')!.map(x => x.event_id);
const m2 = bucketByDay([z, a]).get('2026-09-03')!.map(x => x.event_id);
expect(m1).toEqual(m2);
expect(m1).toEqual(['aaa', 'zzz']);
});
it('时间解析失败的脏数据被跳过而不是让整个视图空白', () => {
const bad = ev({ event_time: '不是时间' });
const good = ev({ event_time: '2026-09-03T09:00:00+08:00' });
const m = bucketByDay([bad, good]);
expect([...m.keys()]).toEqual(['2026-09-03']);
});
it('空输入给空 Map', () => {
expect(bucketByDay([]).size).toBe(0);
});
});
describe('renderReminder', () => {
it('替换三个变量', () => {
const out = renderReminder('【{title}】{time} — {description}', {
title: '发布评审',
description: '看 llmsproxy 的部署脚本',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('【发布评审】2026-09-03 14:30 — 看 llmsproxy 的部署脚本');
});
it('同一变量出现多次全部替换', () => {
// replaceAll 而非 replace后者只换第一个
const out = renderReminder('{title} / {title}', {
title: 'X',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('X / X');
});
it('description 缺失时替换成空串而不是 undefined', () => {
const out = renderReminder('{description}|', {
title: 'T',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('|');
});
it('没有变量的模板原样返回', () => {
const out = renderReminder('纯文本提醒', {
title: 'T',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('纯文本提醒');
});
it('时间解析失败时回退到原始串', () => {
const out = renderReminder('{time}', { title: 'T', event_time: '坏时间' });
expect(out).toBe('坏时间');
});
});
describe('datetime-local 往返', () => {
it('toLocalInput 给本地时区的 YYYY-MM-DDTHH:mm', () => {
const d = new Date(2026, 8, 3, 14, 30);
expect(toLocalInput(d)).toBe('2026-09-03T14:30');
});
it('补零', () => {
const d = new Date(2026, 0, 5, 9, 5);
expect(toLocalInput(d)).toBe('2026-01-05T09:05');
});
it('fromLocalInput 按本地时区解析(人写的就是本地时间)', () => {
const iso = fromLocalInput('2026-09-03T14:30');
// 结果应与本地构造的 Date 一致
expect(iso).toBe(new Date(2026, 8, 3, 14, 30).toISOString());
});
it('往返不丢分钟', () => {
const original = new Date(2026, 8, 3, 14, 30);
expect(toLocalInput(new Date(fromLocalInput(toLocalInput(original))))).toBe('2026-09-03T14:30');
});
it('非法输入给空串', () => {
expect(fromLocalInput('')).toBe('');
expect(fromLocalInput('坏值')).toBe('');
});
});
describe('人类可读描述', () => {
it('重复规则', () => {
expect(describeRecurrence(ev({ recurrence: 'none' }))).toBe('不重复');
expect(describeRecurrence(ev({ recurrence: 'daily' }))).toBe('每天');
expect(describeRecurrence(ev({ recurrence: 'weekly' }))).toBe('每周');
expect(describeRecurrence(ev({ recurrence: 'monthly' }))).toBe('每月');
});
it('带终止时间', () => {
const e = ev({ recurrence: 'daily', recurrence_end: '2026-12-31T00:00:00+08:00' });
expect(describeRecurrence(e)).toBe('每天,至 2026-12-31');
});
it('none 时忽略 recurrence_end', () => {
const e = ev({ recurrence: 'none', recurrence_end: '2026-12-31T00:00:00+08:00' });
expect(describeRecurrence(e)).toBe('不重复');
});
it('提前提醒', () => {
expect(describeRemindBefore(0)).toBe('到点提醒');
expect(describeRemindBefore(15)).toBe('提前 15 分钟');
expect(describeRemindBefore(60)).toBe('提前 1 小时');
expect(describeRemindBefore(120)).toBe('提前 2 小时');
expect(describeRemindBefore(90)).toBe('提前 1 小时 30 分钟');
expect(describeRemindBefore(1440)).toBe('提前 24 小时');
});
});