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 { 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 这一周", ); });