import {describe, expect, it} from "bun:test"; import {addLogbookEntry, dateFormatToRegexSource} from "../src/logbookCore"; import type {LogbookSettings} from "../src/logbookCore"; const settings = (overrides: Partial = {}): LogbookSettings => ({ logbookHeader: "**Logbook**", logbookDateFormat: "YYYY-MM-DD", ...overrides, }); const TODAY = new Date(2026, 5, 1); // 2026-06-01 (local time) function run(lines: string[], cursorLine: number, overrides: Partial = {}) { return addLogbookEntry(lines, cursorLine, settings(overrides), TODAY); } describe("addLogbookEntry – no logbook yet", () => { it("adds a logbook header and a dated entry under a bare task", () => { const edit = run(["- [ ] My task"], 0); expect(edit).not.toBeNull(); expect(edit.lines).toEqual(["- [ ] My task", " - **Logbook**", " - 2026-06-01: "]); expect(edit.cursorLine).toBe(2); expect(edit.cursorCh).toBe(" - 2026-06-01: ".length); }); it("appends the logbook after existing subtasks", () => { const edit = run(["- [ ] My task", " - [ ] subtask"], 0); expect(edit.lines).toEqual([ "- [ ] My task", " - [ ] subtask", " - **Logbook**", " - 2026-06-01: ", ]); }); it("reuses the document's existing indentation unit (tabs)", () => { const edit = run(["- [ ] My task", "\t- [ ] subtask"], 0); expect(edit.lines).toEqual([ "- [ ] My task", "\t- [ ] subtask", "\t- **Logbook**", "\t\t- 2026-06-01: ", ]); }); it("works when invoked from a subtask line (uses that task)", () => { const edit = run(["- [ ] parent", " - [ ] child"], 1); expect(edit.lines).toEqual([ "- [ ] parent", " - [ ] child", " - **Logbook**", " - 2026-06-01: ", ]); }); }); describe("addLogbookEntry – logbook present, no entry for today", () => { it("adds a dated entry to an existing logbook", () => { const edit = run(["- [ ] My task", " - **Logbook**", " - 2026-05-31: did stuff"], 0); expect(edit.lines).toEqual([ "- [ ] My task", " - **Logbook**", " - 2026-05-31: did stuff", " - 2026-06-01: ", ]); expect(edit.cursorLine).toBe(3); expect(edit.cursorCh).toBe(" - 2026-06-01: ".length); }); it("finds the task when the cursor is on a logbook date line", () => { const edit = run(["- [ ] My task", " - **Logbook**", " - 2026-05-31: did stuff"], 2); expect(edit.lines[3]).toBe(" - 2026-06-01: "); }); it("adds today to an empty logbook (header only)", () => { const edit = run(["- [ ] My task", " - **Logbook**"], 0); expect(edit.lines).toEqual(["- [ ] My task", " - **Logbook**", " - 2026-06-01: "]); expect(edit.lines.filter((l) => l.includes("**Logbook**")).length).toBe(1); }); }); describe("addLogbookEntry – entry for today exists", () => { it("converts a single-line entry into a nested list and adds a fresh bullet", () => { const edit = run( ["- [ ] My task", " - **Logbook**", " - 2026-06-01: This and that"], 0, ); expect(edit.lines).toEqual([ "- [ ] My task", " - **Logbook**", " - 2026-06-01:", " - This and that", " - ", ]); expect(edit.cursorLine).toBe(4); expect(edit.cursorCh).toBe(" - ".length); }); it("appends a fresh bullet to a multi-line entry", () => { const edit = run( [ "- [ ] My task", " - **Logbook**", " - 2026-06-01:", " - This and that", " - And this other thing", ], 0, ); expect(edit.lines).toEqual([ "- [ ] My task", " - **Logbook**", " - 2026-06-01:", " - This and that", " - And this other thing", " - ", ]); expect(edit.cursorLine).toBe(5); }); it("places the cursor on an empty single-line entry without adding lines", () => { const edit = run(["- [ ] My task", " - **Logbook**", " - 2026-06-01: "], 0); expect(edit.lines).toEqual(["- [ ] My task", " - **Logbook**", " - 2026-06-01: "]); expect(edit.cursorLine).toBe(2); expect(edit.cursorCh).toBe(" - 2026-06-01: ".length); }); }); describe("addLogbookEntry – edge cases", () => { it("returns null when the cursor is not inside a task", () => { expect(run(["just some text"], 0)).toBeNull(); expect(run(["- a plain bullet"], 0)).toBeNull(); }); it("respects a custom logbook header", () => { const edit = run(["- [ ] My task"], 0, {logbookHeader: "Journal"}); expect(edit.lines).toEqual(["- [ ] My task", " - Journal", " - 2026-06-01: "]); }); it("respects a custom date format", () => { const edit = run(["- [ ] My task"], 0, {logbookDateFormat: "DD/MM/YYYY"}); expect(edit.lines[2]).toBe(" - 01/06/2026: "); }); it("does not confuse a sibling task's logbook", () => { const edit = run( ["- [ ] first", " - **Logbook**", " - 2026-05-30: old", "- [ ] second"], 3, ); expect(edit.lines).toEqual([ "- [ ] first", " - **Logbook**", " - 2026-05-30: old", "- [ ] second", " - **Logbook**", " - 2026-06-01: ", ]); }); }); describe("addLogbookEntry – reuses an existing logbook", () => { function countHeaders(lines: string[]): number { return lines.filter((l) => l.includes("**Logbook**")).length; } it("appends to a sibling logbook written at the task's own indent", () => { const edit = run(["- [ ] My task", "- **Logbook**", " - 2026-05-31: did stuff"], 0); expect(countHeaders(edit.lines)).toBe(1); expect(edit.lines).toEqual([ "- [ ] My task", "- **Logbook**", " - 2026-05-31: did stuff", " - 2026-06-01: ", ]); }); it("appends to a sibling logbook when invoked from one of its date lines", () => { const edit = run(["- [ ] My task", "- **Logbook**", " - 2026-05-31: did stuff"], 2); expect(countHeaders(edit.lines)).toBe(1); expect(edit.lines[3]).toBe(" - 2026-06-01: "); }); it("adds today to an empty sibling logbook (header only)", () => { const edit = run(["- [ ] My task", "- **Logbook**"], 0); expect(countHeaders(edit.lines)).toBe(1); expect(edit.lines).toEqual(["- [ ] My task", "- **Logbook**", " - 2026-06-01: "]); }); it("converts a sibling single-line entry for today into a nested list", () => { const edit = run(["- [ ] My task", "- **Logbook**", " - 2026-06-01: This and that"], 0); expect(countHeaders(edit.lines)).toBe(1); expect(edit.lines).toEqual([ "- [ ] My task", "- **Logbook**", " - 2026-06-01:", " - This and that", " - ", ]); }); it("does not create a second logbook when run repeatedly across days", () => { // Day 1: create from a bare task and type an entry. let lines = run(["- [ ] My task"], 0, {}).lines; lines[2] = " - 2026-05-31: day one note"; // simulate the user typing (run uses TODAY=06-01) // Re-run on a fresh day with the cursor left on the last entry. const day2 = addLogbookEntry(lines, 2, settings(), new Date(2026, 5, 2)); expect(day2).not.toBeNull(); lines = day2.lines; expect(lines.filter((l) => l.includes("**Logbook**")).length).toBe(1); expect(lines).toEqual([ "- [ ] My task", " - **Logbook**", " - 2026-05-31: day one note", " - 2026-06-02: ", ]); }); it("targets the subtask's own logbook, not the parent's", () => { const edit = run( [ "- [ ] parent", " - **Logbook**", " - 2026-05-31: parent note", " - [ ] child", " - **Logbook**", " - 2026-05-31: child note", ], 3, ); expect(edit.lines).toEqual([ "- [ ] parent", " - **Logbook**", " - 2026-05-31: parent note", " - [ ] child", " - **Logbook**", " - 2026-05-31: child note", " - 2026-06-01: ", ]); }); }); describe("addLogbookEntry – indentation configurations", () => { it("creates a logbook with a 4-space indentation unit", () => { const edit = run(["- [ ] Task", " - [ ] Subtask"], 0); expect(edit.lines).toEqual([ "- [ ] Task", " - [ ] Subtask", " - **Logbook**", " - 2026-06-01: ", ]); }); it("creates a logbook on a subtask using the tab unit", () => { const edit = run(["- [ ] Task", "\t- [ ] Subtask"], 1); expect(edit.lines).toEqual([ "- [ ] Task", "\t- [ ] Subtask", "\t\t- **Logbook**", "\t\t\t- 2026-06-01: ", ]); }); it("adds today to a deeply nested logbook on a subtask", () => { const edit = run( [ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-05-31: yesterday", ], 1, ); expect(edit.lines).toEqual([ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-05-31: yesterday", " - 2026-06-01: ", ]); expect(edit.cursorLine).toBe(4); expect(edit.cursorCh).toBe(" - 2026-06-01: ".length); }); it("converts a deeply nested single-line entry for today", () => { const edit = run( [ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-06-01: did a thing", ], 1, ); expect(edit.lines).toEqual([ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-06-01:", " - did a thing", " - ", ]); expect(edit.cursorLine).toBe(5); expect(edit.cursorCh).toBe(" - ".length); }); it("finds a deeply nested logbook from any line within it", () => { const lines = [ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-05-31: yesterday", ]; // cursor on the header line and on a date line both resolve to the subtask for (const cursor of [2, 3]) { const edit = run([...lines], cursor); expect(edit.lines[4]).toBe(" - 2026-06-01: "); } }); it("does not hijack a subtask's logbook when the parent is the target", () => { const edit = run( [ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-05-31: yesterday", ], 0, ); // The parent gets its own logbook at the parent's child indent; the // subtask's logbook is left untouched. expect(edit.lines).toEqual([ "- [/] In progress task", " - [ ] Subtask", " - **Logbook**", " - 2026-05-31: yesterday", " - **Logbook**", " - 2026-06-01: ", ]); expect(edit.lines.filter((l) => l.includes("**Logbook**")).length).toBe(2); }); it("finds the parent's own logbook past a subtask that has its own", () => { const edit = run( [ "- [ ] parent", " - [ ] subtask", " - **Logbook**", " - 2026-05-31: sub note", " - **Logbook**", " - 2026-05-30: parent note", ], 0, ); // Appends to the parent's logbook (line 4), not the subtask's (line 2). expect(edit.lines).toEqual([ "- [ ] parent", " - [ ] subtask", " - **Logbook**", " - 2026-05-31: sub note", " - **Logbook**", " - 2026-05-30: parent note", " - 2026-06-01: ", ]); }); }); describe("dateFormatToRegexSource", () => { it("builds a matcher for the default format", () => { const re = new RegExp(`^${dateFormatToRegexSource("YYYY-MM-DD")}$`); expect(re.test("2026-06-01")).toBe(true); expect(re.test("2026/06/01")).toBe(false); }); it("escapes literal characters", () => { const re = new RegExp(`^${dateFormatToRegexSource("DD.MM.YYYY")}$`); expect(re.test("01.06.2026")).toBe(true); expect(re.test("01x06x2026")).toBe(false); }); });