179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
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 });
|
|
});
|
|
});
|