import collectEvents from "../../Gtd/collectEvents"; import collectKanban from "../../Gtd/collectKanban"; import collectMatrix from "../../Gtd/collectMatrix"; import resolveEffectiveFolderScope from "../../Gtd/effectiveFolderScope"; import parseCalendarConfig from "../../Gtd/parseCalendarConfig"; import parseKanbanConfig from "../../Gtd/parseKanbanConfig"; import parseMatrixConfig from "../../Gtd/parseMatrixConfig"; import { DataAdapter, RawFolder, RawNote } from "../../Gtd/types"; const folders: RawFolder[] = [ { id: "work", parent_id: "", title: "Work" }, { id: "ibm", parent_id: "work", title: "IBM" }, { id: "ibm-child", parent_id: "ibm", title: "Projects" }, { id: "archive-work", parent_id: "work", title: "Archive" }, { id: "personal", parent_id: "", title: "Personal" }, { id: "archive-personal", parent_id: "personal", title: "Archive" }, ]; function note(id: string, parentId: string): RawNote { return { id, title: id, parent_id: parentId, is_todo: 1, todo_due: 0, todo_completed: 0, updated_time: 0, body: "```gtd\n```", }; } function adapter(calls: string[]): DataAdapter { return { getFolders: async () => folders, getNotesInFolder: async (folderId) => { calls.push(folderId); return [note(`note-${folderId}`, folderId)]; }, getNoteTagTitles: async () => [], }; } describe("exclude-notebook config parsing", () => { test.each([ ["calendar", parseCalendarConfig], ["kanban", parseKanbanConfig], ["matrix", parseMatrixConfig], ] as const)("%s accepts omitted, scalar, and list forms", (_name, parse) => { expect(parse({}).excludeNotebooks).toEqual([]); expect(parse({ "exclude-notebook": " IBM " }).excludeNotebooks).toEqual([ "IBM", ]); const config = parse({ "exclude-notebook": [" IBM ", "Archive", "IBM"] }); expect(config.excludeNotebooks).toEqual(["IBM", "Archive", "IBM"]); expect(config.warnings).toEqual([]); }); test.each([parseCalendarConfig, parseKanbanConfig, parseMatrixConfig])( "retains valid list entries and warns in input order", (parse) => { const config = parse({ "exclude-notebook": ["IBM", " ", { title: "Archive" }, ["Personal"]], }); expect(config.excludeNotebooks).toEqual(["IBM"]); expect(config.warnings).toEqual([ "Invalid exclude-notebook entry 2 (empty value ignored)", "Invalid exclude-notebook entry 3 (expected a notebook name, path, or id)", "Invalid exclude-notebook entry 4 (expected a notebook name, path, or id)", ]); } ); }); describe("effective notebook scope", () => { test("subtracts complete, overlapping trees from scope: all", () => { const result = resolveEffectiveFolderScope( folders, "work", 0, true, ["IBM", "Work/IBM/Projects", "IBM"] ); expect(result.folderIds).toEqual([ "work", "archive-work", "personal", "archive-personal", ]); expect([...result.excludedFolderIds].sort()).toEqual(["ibm", "ibm-child"]); expect(result.warnings).toEqual([]); }); test("resolves entries independently and warns without fallback", () => { const result = resolveEffectiveFolderScope( folders, "work", Infinity, false, ["Archive", "Nope", "Personal"] ); expect(result.folderIds).toEqual(["work", "ibm", "archive-work", "ibm-child"]); expect(result.warnings).toEqual([ 'exclude-notebook: "Archive" is ambiguous (2 notebooks share that name — qualify it as a Parent/Child path) (nothing excluded)', 'exclude-notebook: no notebook named "Nope" (nothing excluded)', ]); }); test("supports paths, root exclusion, empty scope, and cycle safety", () => { expect( resolveEffectiveFolderScope(folders, "work", Infinity, false, ["Work"]).folderIds ).toEqual([]); const cyclic: RawFolder[] = [ { id: "a", parent_id: "b", title: "A" }, { id: "b", parent_id: "a", title: "B" }, ]; expect( resolveEffectiveFolderScope(cyclic, "a", Infinity, false, ["b"]).folderIds ).toEqual([]); }); test("accepts a 32-character folder id", () => { const id = "0123456789abcdef0123456789abcdef"; const idFolders = [ ...folders, { id, parent_id: "personal", title: "Secret" }, ]; const result = resolveEffectiveFolderScope( idFolders, "work", 0, true, [id.toUpperCase()] ); expect(result.folderIds).not.toContain(id); expect(result.warnings).toEqual([]); }); }); describe("collector exclusion integration", () => { test.each([ ["calendar", async (data: DataAdapter) => collectEvents(data, "host", "work", parseCalendarConfig({ scope: "all", notes: "all", todos: "all", "exclude-notebook": "IBM" }))], ["kanban", async (data: DataAdapter) => collectKanban(data, "host", "work", parseKanbanConfig({ scope: "all", todos: "all", "exclude-notebook": "IBM" }))], ["matrix", async (data: DataAdapter) => collectMatrix(data, "host", "work", parseMatrixConfig({ scope: "all", todos: "all", mode: "eisenhower", "exclude-notebook": "IBM" }))], ] as const)("%s never fetches excluded folders", async (_name, collect) => { const calls: string[] = []; const result = await collect(adapter(calls)); expect(calls).toEqual(["work", "archive-work", "personal", "archive-personal"]); expect(result.scannedFolders).toBe(4); expect(result.scannedNotes).toBe(4); }); test.each([ ["kanban", async (data: DataAdapter) => collectKanban(data, "host", "work", parseKanbanConfig({ scope: "children", group: "notebook", todos: "all", "exclude-notebook": "IBM" }))], ["matrix", async (data: DataAdapter) => collectMatrix(data, "host", "work", parseMatrixConfig({ scope: "children", group: "notebook", todos: "all", mode: "eisenhower", "exclude-notebook": "IBM" }))], ] as const)("%s grouped layout omits the excluded tree", async (_name, collect) => { const calls: string[] = []; const result = await collect(adapter(calls)); expect(calls).toEqual(["work", "archive-work"]); expect(result.layout.kind).toBe("notebooks"); if (result.layout.kind !== "notebooks") throw new Error("grouped expected"); expect(result.layout.groups.map((group) => group.folderId)).toEqual([ "work", "archive-work", ]); }); test("an entirely excluded calendar scope uses an empty result", async () => { const calls: string[] = []; const result = await collectEvents( adapter(calls), "host", "work", parseCalendarConfig({ "exclude-notebook": "Work" }) ); expect(calls).toEqual([]); expect(result).toMatchObject({ events: [], scannedFolders: 0, scannedNotes: 0 }); }); });