When the logbook feature is enabled, a pending task's logbook is kept and carried along with the task during rollover instead of being pruned out as a non-task bullet. Ownership is precise: a subtask's logbook stays with the subtask, and a completed task's logbook is not migrated.
545 lines
19 KiB
TypeScript
545 lines
19 KiB
TypeScript
import {describe, expect, it} from "bun:test";
|
||
import {
|
||
appendRolledOver,
|
||
buildRolloverPlan,
|
||
formatDate,
|
||
parseDateFromName,
|
||
RolloverPlanSettings,
|
||
} from "../src/rolloverCore";
|
||
|
||
const settings = (overrides: Partial<RolloverPlanSettings> = {}): RolloverPlanSettings => ({
|
||
completedSymbol: "x",
|
||
inProgressSymbol: "/",
|
||
movedSymbol: ">",
|
||
rolloverSourceAction: "mark-moved",
|
||
rolloverContext: "topmost-element",
|
||
enableLogbook: true,
|
||
logbookHeader: "**Logbook**",
|
||
...overrides,
|
||
});
|
||
|
||
describe("formatDate", () => {
|
||
it("formats with default YYYY-MM-DD", () => {
|
||
expect(formatDate(new Date(2026, 3, 5), "YYYY-MM-DD")).toBe("2026-04-05");
|
||
});
|
||
|
||
it("zero-pads month and day", () => {
|
||
expect(formatDate(new Date(2026, 0, 1), "YYYY-MM-DD")).toBe("2026-01-01");
|
||
});
|
||
|
||
it("supports custom separators", () => {
|
||
expect(formatDate(new Date(2026, 11, 31), "DD/MM/YYYY")).toBe("31/12/2026");
|
||
});
|
||
|
||
it("supports literal text", () => {
|
||
expect(formatDate(new Date(2026, 3, 5), "daily-YYYY-MM-DD")).toBe("daily-2026-04-05");
|
||
});
|
||
});
|
||
|
||
describe("parseDateFromName", () => {
|
||
it("parses YYYY-MM-DD", () => {
|
||
const d = parseDateFromName("2026-04-05", "YYYY-MM-DD");
|
||
expect(d).not.toBeNull();
|
||
expect(d.getFullYear()).toBe(2026);
|
||
expect(d.getMonth()).toBe(3);
|
||
expect(d.getDate()).toBe(5);
|
||
});
|
||
|
||
it("returns null for non-matching name", () => {
|
||
expect(parseDateFromName("not-a-date", "YYYY-MM-DD")).toBeNull();
|
||
});
|
||
|
||
it("returns null when format is wrong", () => {
|
||
expect(parseDateFromName("04/05/2026", "YYYY-MM-DD")).toBeNull();
|
||
});
|
||
|
||
it("rejects out-of-range dates", () => {
|
||
expect(parseDateFromName("2026-13-01", "YYYY-MM-DD")).toBeNull();
|
||
expect(parseDateFromName("2026-02-30", "YYYY-MM-DD")).toBeNull();
|
||
});
|
||
|
||
it("supports custom format", () => {
|
||
const d = parseDateFromName("31/12/2026", "DD/MM/YYYY");
|
||
expect(d).not.toBeNull();
|
||
expect(d.getFullYear()).toBe(2026);
|
||
expect(d.getMonth()).toBe(11);
|
||
expect(d.getDate()).toBe(31);
|
||
});
|
||
|
||
it("escapes regex metacharacters in format", () => {
|
||
const d = parseDateFromName("2026.04.05", "YYYY.MM.DD");
|
||
expect(d).not.toBeNull();
|
||
expect(d.getFullYear()).toBe(2026);
|
||
});
|
||
|
||
it("round-trips with formatDate", () => {
|
||
const original = new Date(2026, 6, 14);
|
||
const formatted = formatDate(original, "YYYY-MM-DD");
|
||
const parsed = parseDateFromName(formatted, "YYYY-MM-DD");
|
||
expect(parsed).not.toBeNull();
|
||
expect(parsed.getTime()).toBe(original.getTime());
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – no migration", () => {
|
||
it("leaves content unchanged when there are no tasks", () => {
|
||
const input = "# Title\n\nJust some prose.\n";
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.newContent).toBe(input);
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
});
|
||
|
||
it("does not migrate when only completed tasks exist", () => {
|
||
const input = "- [x] done\n- [x] also done\n";
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
});
|
||
|
||
it("does not migrate when only discarded tasks exist", () => {
|
||
const input = "- [-] discarded\n";
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
});
|
||
|
||
it("does not migrate already-moved tasks", () => {
|
||
const input = "- [>] moved already\n";
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – pending detection", () => {
|
||
it("treats [ ] (open) as pending", () => {
|
||
const plan = buildRolloverPlan("- [ ] open\n", settings({rolloverContext: "none"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] open"]]);
|
||
});
|
||
|
||
it("treats configured in-progress symbol as pending", () => {
|
||
const plan = buildRolloverPlan("- [/] in progress\n", settings({rolloverContext: "none"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [/] in progress"]]);
|
||
});
|
||
|
||
it("respects a custom in-progress symbol", () => {
|
||
const custom = settings({inProgressSymbol: "?", rolloverContext: "none"});
|
||
const plan = buildRolloverPlan("- [?] custom\n- [/] not pending now\n", custom);
|
||
expect(plan.migratedBlocks).toEqual([["- [?] custom"]]);
|
||
});
|
||
|
||
it("ignores discarded [-] tasks", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n- [-] discarded\n",
|
||
settings({rolloverContext: "none"}),
|
||
);
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] open"]]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – source action", () => {
|
||
it("marks pending tasks as moved (default)", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n- [x] done\n",
|
||
settings({rolloverContext: "none"}),
|
||
);
|
||
expect(plan.newContent).toBe("- [>] open\n- [x] done\n");
|
||
});
|
||
|
||
it("removes pending tasks when action is remove", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n- [x] done\n",
|
||
settings({rolloverSourceAction: "remove", rolloverContext: "none"}),
|
||
);
|
||
expect(plan.newContent).toBe("- [x] done\n");
|
||
});
|
||
|
||
it("uses a custom moved symbol", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n",
|
||
settings({movedSymbol: "~", rolloverContext: "none"}),
|
||
);
|
||
expect(plan.newContent).toBe("- [~] open\n");
|
||
});
|
||
|
||
it("does not touch non-pending tasks in the source", () => {
|
||
const input = "- This\n - [ ] pending\n - [x] done\n - [-] discarded\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.newContent).toBe("- This\n - [>] pending\n - [x] done\n - [-] discarded\n");
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – surrounding context", () => {
|
||
const input = ["# H1", "## H2", "- Group", " - [ ] task", ""].join("\n");
|
||
|
||
it("none migrates only the task subtree", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "none"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-task behaves like none", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-task"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-list returns the topmost non-task list ancestor subtree", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([["- Group", " - [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-list falls back to root task when no list ancestor", () => {
|
||
const plain = "## H2\n- [ ] task\n";
|
||
const plan = buildRolloverPlan(plain, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-heading returns the outermost heading section", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["# H1", "## H2", "- Group", " - [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-heading falls back to root task when no heading", () => {
|
||
const plain = "- Group\n - [ ] task\n";
|
||
const plan = buildRolloverPlan(plain, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
|
||
it("nearest-heading returns the closest heading section above the task", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "nearest-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["## H2", "- Group", " - [ ] task"]]);
|
||
});
|
||
|
||
it("nearest-heading falls back to root task when no heading", () => {
|
||
const plain = "- Group\n - [ ] task\n";
|
||
const plan = buildRolloverPlan(plain, settings({rolloverContext: "nearest-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-element prefers heading over list ancestor", () => {
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-element"}));
|
||
expect(plan.migratedBlocks).toEqual([["# H1", "## H2", "- Group", " - [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-element uses list when no heading is present", () => {
|
||
const noHeading = "- Group\n - [ ] task\n";
|
||
const plan = buildRolloverPlan(noHeading, settings({rolloverContext: "topmost-element"}));
|
||
expect(plan.migratedBlocks).toEqual([["- Group", " - [ ] task"]]);
|
||
});
|
||
|
||
it("topmost-element falls back to task subtree when neither heading nor list", () => {
|
||
const bare = "- [ ] task\n";
|
||
const plan = buildRolloverPlan(bare, settings({rolloverContext: "topmost-element"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] task"]]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – tree pruning", () => {
|
||
it("user example: prunes branches without pending tasks", () => {
|
||
const input = [
|
||
"- This",
|
||
" - [ ] tasks pending",
|
||
" - Heading",
|
||
" - [ ] Pending",
|
||
" - Head 2",
|
||
" - [x] Completed",
|
||
" - Head 3",
|
||
" - Comment",
|
||
" - Comment 2",
|
||
" - [x] Completed",
|
||
"",
|
||
].join("\n");
|
||
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([
|
||
["- This", " - [ ] tasks pending", " - Heading", " - [ ] Pending"],
|
||
]);
|
||
});
|
||
|
||
it("keeps non-task containers that have pending descendants", () => {
|
||
const input = "- Group\n - [ ] open\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([["- Group", " - [ ] open"]]);
|
||
});
|
||
|
||
it("drops non-task containers with only completed children", () => {
|
||
const input = [
|
||
"- Outer",
|
||
" - Empty group",
|
||
" - [x] done",
|
||
" - Active group",
|
||
" - [ ] open",
|
||
"",
|
||
].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([["- Outer", " - Active group", " - [ ] open"]]);
|
||
});
|
||
|
||
it("keeps a heading when one of its tasks is pending", () => {
|
||
const input = "## Plan\n- [x] done\n- [ ] open\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["## Plan", "- [ ] open"]]);
|
||
});
|
||
|
||
it("drops completed tasks even within the migrated block", () => {
|
||
const input = "## Plan\n- [ ] open\n- [x] done\n- [-] discarded\n- [ ] also open\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["## Plan", "- [ ] open", "- [ ] also open"]]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – multiple anchors", () => {
|
||
it("emits separate blocks for separate root tasks with no surrounding context", () => {
|
||
const input = "- [ ] first\n\n- [ ] second\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "none"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] first"], ["- [ ] second"]]);
|
||
});
|
||
|
||
it("merges overlapping anchors (two pending under same heading)", () => {
|
||
const input = "## Plan\n- [ ] one\n- [ ] two\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["## Plan", "- [ ] one", "- [ ] two"]]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – dedent", () => {
|
||
it("dedents migrated content by the anchor's indent", () => {
|
||
const input = " - Outer\n - [ ] task\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-list"}));
|
||
expect(plan.migratedBlocks).toEqual([["- Outer", " - [ ] task"]]);
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – migration footer", () => {
|
||
it("appends the migration footer to the source when target is provided", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n",
|
||
settings({rolloverContext: "none"}),
|
||
"2026-04-29",
|
||
);
|
||
expect(plan.newContent).toBe(
|
||
"- [>] open\n\nTasks were automatically migrated to [[2026-04-29]]\n",
|
||
);
|
||
});
|
||
|
||
it("does not append a footer when no target is provided", () => {
|
||
const plan = buildRolloverPlan("- [ ] open\n", settings({rolloverContext: "none"}));
|
||
expect(plan.newContent).toBe("- [>] open\n");
|
||
});
|
||
|
||
it("does not append a footer when nothing was migrated", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [x] done\n",
|
||
settings({rolloverContext: "none"}),
|
||
"2026-04-29",
|
||
);
|
||
expect(plan.newContent).toBe("- [x] done\n");
|
||
});
|
||
|
||
it("does not duplicate an existing footer for the same target", () => {
|
||
const input = "- [ ] open\n\nTasks were automatically migrated to [[2026-04-29]]\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "none"}), "2026-04-29");
|
||
expect(plan.newContent).toBe(
|
||
"- [>] open\n\nTasks were automatically migrated to [[2026-04-29]]\n",
|
||
);
|
||
});
|
||
|
||
it("appends a new footer when targeting a different note", () => {
|
||
const input = "- [ ] open\n\nTasks were automatically migrated to [[2026-04-28]]\n";
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "none"}), "2026-04-29");
|
||
expect(plan.newContent).toBe(
|
||
"- [>] open\n\nTasks were automatically migrated to [[2026-04-28]]\n\nTasks were automatically migrated to [[2026-04-29]]\n",
|
||
);
|
||
});
|
||
|
||
it("works alongside the remove source action", () => {
|
||
const plan = buildRolloverPlan(
|
||
"- [ ] open\n",
|
||
settings({rolloverContext: "none", rolloverSourceAction: "remove"}),
|
||
"2026-04-29",
|
||
);
|
||
expect(plan.newContent).toBe("Tasks were automatically migrated to [[2026-04-29]]\n");
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – code blocks and frontmatter", () => {
|
||
it("does not migrate task-like lines inside a fenced code block", () => {
|
||
const input = ["```markdown", "- [ ] not real", "```", ""].join("\n");
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
expect(plan.newContent).toBe(input);
|
||
});
|
||
|
||
it("does not migrate task-like lines inside a tilde-fenced code block", () => {
|
||
const input = ["~~~", "- [ ] not real", "~~~", ""].join("\n");
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
expect(plan.newContent).toBe(input);
|
||
});
|
||
|
||
it("treats code-block content as inert when migrating a heading section", () => {
|
||
const input = ["## Plan", "- [ ] real", "```", "- [ ] code task", "```", ""].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["## Plan", "- [ ] real"]]);
|
||
});
|
||
|
||
it("does not treat a heading inside a code block as a heading ancestor", () => {
|
||
const input = ["```", "# Fake heading", "```", "- [ ] real"].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] real"]]);
|
||
});
|
||
|
||
it("does not mark code-block tasks as moved in the source", () => {
|
||
const input = ["```", "- [ ] not real", "```", "- [ ] real"].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "none"}));
|
||
expect(plan.newContent).toBe(["```", "- [ ] not real", "```", "- [>] real"].join("\n"));
|
||
});
|
||
|
||
it("ignores task-like lines inside YAML frontmatter", () => {
|
||
const input = ["---", "- [ ] not a task", "---", "- [x] done"].join("\n");
|
||
const plan = buildRolloverPlan(input, settings());
|
||
expect(plan.migratedBlocks).toEqual([]);
|
||
expect(plan.newContent).toBe(input);
|
||
});
|
||
});
|
||
|
||
describe("appendRolledOver", () => {
|
||
const block = (lines: string[]) => lines;
|
||
|
||
it("returns destination unchanged when no blocks", () => {
|
||
expect(appendRolledOver("hello\n", [], "## Past tasks")).toBe("hello\n");
|
||
});
|
||
|
||
it("inserts header and content into an empty destination", () => {
|
||
const result = appendRolledOver("", [block(["- [ ] x"])], "## Past tasks");
|
||
expect(result).toBe("## Past tasks\n\n- [ ] x\n");
|
||
});
|
||
|
||
it("appends header and content when not present", () => {
|
||
const result = appendRolledOver("# Day\n", [block(["- [ ] x"])], "## Past tasks");
|
||
expect(result).toBe("# Day\n\n## Past tasks\n\n- [ ] x\n");
|
||
});
|
||
|
||
it("appends content under existing header", () => {
|
||
const dest = "# Day\n\n## Past tasks\n\n- [ ] earlier\n";
|
||
const result = appendRolledOver(dest, [block(["- [ ] new"])], "## Past tasks");
|
||
expect(result).toBe("# Day\n\n## Past tasks\n\n- [ ] earlier\n- [ ] new\n");
|
||
});
|
||
|
||
it("joins multiple blocks", () => {
|
||
const result = appendRolledOver(
|
||
"",
|
||
[block(["- [ ] a"]), block(["- [ ] b"])],
|
||
"## Past tasks",
|
||
);
|
||
expect(result).toBe("## Past tasks\n\n- [ ] a\n- [ ] b\n");
|
||
});
|
||
|
||
it("normalizes destination missing trailing newline", () => {
|
||
const result = appendRolledOver("# Day", [block(["- [ ] x"])], "## Past tasks");
|
||
expect(result).toBe("# Day\n\n## Past tasks\n\n- [ ] x\n");
|
||
});
|
||
});
|
||
|
||
describe("buildRolloverPlan – logbook migration", () => {
|
||
const nested = [
|
||
"- [ ] My task",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: did stuff",
|
||
" - 2026-06-01: more",
|
||
].join("\n");
|
||
|
||
it("migrates a nested logbook along with its task", () => {
|
||
const plan = buildRolloverPlan(nested, settings({rolloverContext: "topmost-task"}));
|
||
expect(plan.migratedBlocks).toEqual([
|
||
[
|
||
"- [ ] My task",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: did stuff",
|
||
" - 2026-06-01: more",
|
||
],
|
||
]);
|
||
});
|
||
|
||
it("migrates the logbook regardless of surrounding-context setting", () => {
|
||
for (const ctx of ["none", "topmost-list", "topmost-element"] as const) {
|
||
const plan = buildRolloverPlan(nested, settings({rolloverContext: ctx}));
|
||
expect(plan.migratedBlocks[0]).toContain(" - **Logbook**");
|
||
expect(plan.migratedBlocks[0]).toContain(" - 2026-05-31: did stuff");
|
||
}
|
||
});
|
||
|
||
it("dedents a deeply nested logbook with the task", () => {
|
||
const input = ["## Work", "- [ ] task", " - **Logbook**", " - 2026-05-31: note"].join(
|
||
"\n",
|
||
);
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-heading"}));
|
||
expect(plan.migratedBlocks).toEqual([
|
||
["## Work", "- [ ] task", " - **Logbook**", " - 2026-05-31: note"],
|
||
]);
|
||
});
|
||
|
||
it("leaves the logbook in the source under the moved task (mark-moved)", () => {
|
||
const plan = buildRolloverPlan(nested, settings({rolloverContext: "topmost-task"}));
|
||
expect(plan.newContent).toBe(
|
||
[
|
||
"- [>] My task",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: did stuff",
|
||
" - 2026-06-01: more",
|
||
].join("\n"),
|
||
);
|
||
});
|
||
|
||
it("does not migrate a completed task's logbook", () => {
|
||
const input = [
|
||
"- [x] done",
|
||
" - **Logbook**",
|
||
" - 2026-05-30: history",
|
||
"- [ ] todo",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: note",
|
||
].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-task"}));
|
||
expect(plan.migratedBlocks).toEqual([
|
||
["- [ ] todo", " - **Logbook**", " - 2026-05-31: note"],
|
||
]);
|
||
});
|
||
|
||
it("keeps each task's own logbook when a subtask has one too", () => {
|
||
const input = [
|
||
"- [ ] parent",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: parent log",
|
||
" - [ ] child",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: child log",
|
||
].join("\n");
|
||
const plan = buildRolloverPlan(input, settings({rolloverContext: "topmost-task"}));
|
||
expect(plan.migratedBlocks).toEqual([
|
||
[
|
||
"- [ ] parent",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: parent log",
|
||
" - [ ] child",
|
||
" - **Logbook**",
|
||
" - 2026-05-31: child log",
|
||
],
|
||
]);
|
||
});
|
||
|
||
it("honors a custom logbook header", () => {
|
||
const input = ["- [ ] task", " - Journal", " - 2026-05-31: note"].join("\n");
|
||
const plan = buildRolloverPlan(
|
||
input,
|
||
settings({rolloverContext: "topmost-task", logbookHeader: "Journal"}),
|
||
);
|
||
expect(plan.migratedBlocks).toEqual([
|
||
["- [ ] task", " - Journal", " - 2026-05-31: note"],
|
||
]);
|
||
});
|
||
|
||
it("drops the logbook when the feature is disabled", () => {
|
||
const plan = buildRolloverPlan(
|
||
nested,
|
||
settings({rolloverContext: "topmost-task", enableLogbook: false}),
|
||
);
|
||
expect(plan.migratedBlocks).toEqual([["- [ ] My task"]]);
|
||
});
|
||
});
|