Files
plainleaf/client/components/plainleaf_tasks.test.ts
T
wushenghua 9e43ae824a
CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
feat: complete task planning and account management
Add Markdown-native planning, repeat rules, subtasks, reminders, transactional backup restore, Chinese account management, and the account-managed NAS migration configuration.\n\nCo-Authored-By: Codex <noreply@anthropic.com>
2026-08-12 20:22:33 +08:00

263 lines
8.8 KiB
TypeScript

import { expect, test } from "vitest";
import {
calendarMonth,
dueReminderTasks,
groupScheduledTasks,
groupLogbookTasks,
groupTasksByProject,
isOverdueTask,
localDateKey,
logbookTasks,
nextRepeatDate,
nextRepeatedTask,
parseNaturalTaskDate,
projectForTask,
projectSummaries,
sortTodayTasks,
staleProjects,
subtaskProgress,
taskBelongsToToday,
tasksForAnytime,
tasksForInbox,
tasksForCalendarDate,
taskHealthIssues,
taskMatchesFilters,
taskDepth,
type PlainleafTask,
} from "./plainleaf_tasks.ts";
function task(extra: Partial<PlainleafTask> = {}): PlainleafTask {
return {
ref: "测试@0",
page: "测试",
name: "测试任务",
state: " ",
done: false,
...extra,
};
}
test("localDateKey uses local calendar fields", () => {
expect(localDateKey(new Date(2026, 7, 12, 23, 30))).toBe("2026-08-12");
});
test("today contains scheduled today and overdue or due deadlines", () => {
const today = "2026-08-12";
expect(taskBelongsToToday(task({ scheduled: today }), today)).toBe(true);
expect(taskBelongsToToday(task({ deadline: "2026-08-10" }), today)).toBe(
true,
);
expect(taskBelongsToToday(task({ deadline: today }), today)).toBe(true);
expect(taskBelongsToToday(task({ scheduled: "2026-08-13" }), today)).toBe(
false,
);
expect(
taskBelongsToToday(task({ scheduled: today, done: true }), today),
).toBe(false);
expect(isOverdueTask(task({ deadline: "2026-08-11" }), today)).toBe(true);
});
test("sortTodayTasks puts overdue and due tasks before scheduled tasks", () => {
const today = "2026-08-12";
expect(
sortTodayTasks(
[
task({ ref: "scheduled", name: "计划", scheduled: today }),
task({ ref: "due", name: "截止", deadline: today }),
task({ ref: "overdue", name: "逾期", deadline: "2026-08-11" }),
],
today,
).map((item) => item.ref),
).toEqual(["overdue", "due", "scheduled"]);
});
test("inbox and anytime remain distinct", () => {
const inbox = task({ ref: "inbox", page: "任务/收件箱" });
const anytime = task({ ref: "anytime", page: "生活/采购" });
const planned = task({
ref: "planned",
page: "生活/采购",
scheduled: "2026-08-13",
});
expect(tasksForInbox([inbox, anytime, planned])).toEqual([inbox]);
expect(tasksForAnytime([inbox, anytime, planned])).toEqual([anytime]);
});
test("planned tasks are grouped by the next meaningful date", () => {
const groups = groupScheduledTasks(
[
task({ ref: "a", scheduled: "2026-08-14" }),
task({ ref: "b", deadline: "2026-08-13" }),
task({ ref: "c", scheduled: "2026-08-13", deadline: "2026-08-20" }),
task({ ref: "old", deadline: "2026-08-11" }),
],
"2026-08-12",
);
expect(
groups.map((group) => [group.date, group.tasks.map((item) => item.ref)]),
).toEqual([
["2026-08-13", ["b", "c"]],
["2026-08-14", ["a"]],
]);
});
test("explicit wiki links override source pages as projects", () => {
expect(
projectForTask(task({ page: "工作/周会", links: ["项目/Plainleaf"] })),
).toBe("项目/Plainleaf");
expect(projectForTask(task({ page: "工作/周会" }))).toBe("工作/周会");
expect(projectForTask(task({ page: "任务/收件箱" }))).toBeUndefined();
expect(
groupTasksByProject([
task({ ref: "a", page: "工作", links: ["项目 A"] }),
task({ ref: "b", page: "项目 B" }),
]).map((group) => group.project),
).toEqual(["项目 A", "项目 B"]);
});
test("logbook sorts completed metadata before source modification time", () => {
expect(
logbookTasks([
task({ ref: "old", done: true, completed: "2026-08-10" }),
task({ ref: "new", done: true, completed: "2026-08-12" }),
task({ ref: "open" }),
]).map((item) => item.ref),
).toEqual(["new", "old"]);
});
test("parses predictable Chinese quick dates without guessing", () => {
const base = new Date(2026, 7, 12, 10); // Wednesday
expect(parseNaturalTaskDate("今天", base)).toBe("2026-08-12");
expect(parseNaturalTaskDate("明天", base)).toBe("2026-08-13");
expect(parseNaturalTaskDate("后天", base)).toBe("2026-08-14");
expect(parseNaturalTaskDate("周五", base)).toBe("2026-08-14");
expect(parseNaturalTaskDate("本周一", base)).toBe("2026-08-10");
expect(parseNaturalTaskDate("下周一", base)).toBe("2026-08-17");
expect(parseNaturalTaskDate("8月20日", base)).toBe("2026-08-20");
expect(parseNaturalTaskDate("2026-99-99", base)).toBeUndefined();
expect(parseNaturalTaskDate("晚一点", base)).toBeUndefined();
});
test("calculates repeat dates including month ends and workdays", () => {
expect(nextRepeatDate("daily", "2026-08-12")).toBe("2026-08-13");
expect(nextRepeatDate("weekdays", "2026-08-14")).toBe("2026-08-17");
expect(nextRepeatDate("weekly", "2026-08-12")).toBe("2026-08-19");
expect(nextRepeatDate("monthly", "2026-01-31")).toBe("2026-02-28");
expect(nextRepeatDate("yearly", "2024-02-29")).toBe("2025-02-28");
});
test("creates the next portable Markdown task for a completed repeat", () => {
expect(
nextRepeatedTask(
task({
name: "周报",
scheduled: "2026-08-12",
deadline: "2026-08-13",
repeat: "weekly",
reminder: "2026-08-12T09:30",
tags: ["工作"],
links: ["项目/周报"],
}),
),
).toEqual({
name: "周报",
scheduled: "2026-08-19",
deadline: "2026-08-20",
repeat: "weekly",
reminder: "2026-08-19T09:30",
tags: ["工作"],
project: "项目/周报",
});
expect(
nextRepeatedTask(
task({ repeat: "daily", scheduled: "2026-08-12", deadline: "bad" }),
)?.deadline,
).toBeUndefined();
expect(
nextRepeatedTask(task({ repeat: "daily", deadline: "2026-08-12" })),
).toMatchObject({ scheduled: undefined, deadline: "2026-08-13" });
});
test("filters tasks and calculates bounded subtask depth", () => {
const parent = task({ ref: "parent", name: "发布", tags: ["工作"] });
const child = task({ ref: "child", parent: "parent", name: "检查文案" });
const grandchild = task({ ref: "grandchild", parent: "child" });
expect(taskMatchesFilters(parent, { query: "发布", tag: "工作" })).toBe(true);
expect(taskMatchesFilters(parent, { query: "生活" })).toBe(false);
expect(taskDepth(grandchild, [parent, child, grandchild])).toBe(2);
expect(
subtaskProgress(parent, [parent, { ...child, done: true }, grandchild]),
).toEqual({ completed: 1, total: 2 });
expect(subtaskProgress(child, [parent, child])).toBeUndefined();
});
test("builds a stable six-week calendar and locates dated tasks", () => {
const days = calendarMonth(2026, 7);
expect(days).toHaveLength(42);
expect(days[0]).toEqual({ date: "2026-07-26", inMonth: false });
expect(days.at(-1)).toEqual({ date: "2026-09-05", inMonth: false });
expect(
tasksForCalendarDate(
[task({ ref: "a", scheduled: "2026-08-12" }), task({ ref: "b" })],
"2026-08-12",
).map((item) => item.ref),
).toEqual(["a"]);
});
test("flags stale projects and recoverable task data problems", () => {
expect(
staleProjects(
[task({ page: "项目 A", pageLastModified: "2026-07-01" })],
"2026-08-12",
),
).toEqual(new Set(["项目 A"]));
const issues = taskHealthIssues([
task({ ref: "dup", scheduled: "2026-99-12" }),
task({ ref: "dup", scheduled: "2026-08-20", deadline: "2026-08-19" }),
task({ ref: "orphan", parent: "missing" }),
]);
expect(new Set(issues.map((issue) => issue.type))).toEqual(
new Set([
"invalid-date",
"deadline-before-scheduled",
"missing-parent",
"duplicate-ref",
]),
);
});
test("finds due reminders once and ignores completed or invalid entries", () => {
const notified = new Set(["old:2026-08-12T09:00"]);
expect(
dueReminderTasks(
[
task({ ref: "due", reminder: "2026-08-12T09:30" }),
task({ ref: "future", reminder: "2026-08-12T11:00" }),
task({ ref: "old", reminder: "2026-08-12T09:00" }),
task({ ref: "done", reminder: "2026-08-12T09:00", done: true }),
task({ ref: "bad", reminder: "not-a-date" }),
],
new Date("2026-08-12T10:00:00"),
notified,
).map((item) => item.ref),
).toEqual(["due"]);
});
test("summarizes project progress and groups logbook by week or month", () => {
const entries = [
task({ ref: "open", page: "项目 A" }),
task({ ref: "done", page: "项目 A", done: true, completed: "2026-08-12" }),
task({ ref: "older", page: "项目 B", done: true, completed: "2026-07-31" }),
];
expect(projectSummaries(entries)).toEqual([
{ project: "项目 A", open: 1, completed: 1, total: 2 },
{ project: "项目 B", open: 0, completed: 1, total: 1 },
]);
expect(
groupLogbookTasks(entries, "month").map((group) => group.period),
).toEqual(["2026年8月", "2026年7月"]);
expect(groupLogbookTasks(entries, "week")[0].period).toBe(
"2026-08-10 这一周",
);
});