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
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>
263 lines
8.8 KiB
TypeScript
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 这一周",
|
|
);
|
|
});
|