import { moveMatrixItem } from "../../Gtd/matrixMutation"; import { MatrixDestination, MutationAdapter, RawNote, RawTag } from "../../Gtd/types"; function note(isTodo: boolean): RawNote { return { id: "card", title: "Card", parent_id: "folder", is_todo: isTodo ? 1 : 0, todo_due: 123, todo_completed: 0, updated_time: 456, body: "```gtd\n```", }; } function fakeAdapter(current: RawNote | null, attached: RawTag[], all?: RawTag[]) { const calls: string[] = []; const adapter: MutationAdapter = { getNote: async () => current, getNoteTags: async () => attached.slice(), getAllTags: async () => (all || [ { id: "u", title: "urgent" }, { id: "i", title: "important" }, ]).slice(), createTag: async (title) => { calls.push(`create:${title}`); return { id: title, title }; }, attachTag: async (tagId) => { calls.push(`attach:${tagId}`); }, detachTag: async (tagId) => { calls.push(`detach:${tagId}`); }, setTodoCompleted: async (_noteId, completedTime) => { calls.push(`complete:${completedTime > 0}`); }, }; return { adapter, calls }; } const states: Array<{ destination: MatrixDestination; urgent: boolean; important: boolean; }> = [ { destination: "topLeft", urgent: true, important: true }, { destination: "topRight", urgent: false, important: true }, { destination: "bottomLeft", urgent: true, important: false }, { destination: "bottomRight", urgent: false, important: false }, ]; const config = { mode: "eisenhower" as const, urgentTag: "urgent", importantTag: "important", inProgressTag: "in-progress", onDeckTag: "on-deck", doneTag: "done", }; describe("moveMatrixItem", () => { for (const isTodo of [false, true]) { for (const source of states) { for (const target of states) { test(`${isTodo ? "to-do" : "note"}: ${source.destination} -> ${target.destination}`, async () => { const tags: RawTag[] = []; if (source.urgent) tags.push({ id: "u", title: "urgent" }); if (source.important) tags.push({ id: "i", title: "important" }); const { adapter, calls } = fakeAdapter(note(isTodo), tags); const result = await moveMatrixItem(adapter, "card", target.destination, config); const expected: string[] = []; if (source.urgent !== target.urgent) expected.push(`${target.urgent ? "attach" : "detach"}:u`); if (source.important !== target.important) expected.push(`${target.important ? "attach" : "detach"}:i`); expect(calls).toEqual(expected); expect(result.changed).toBe(source.destination !== target.destination); expect(result.note).toMatchObject({ is_todo: isTodo ? 1 : 0, todo_due: 123, todo_completed: 0, }); }); } } } test("uses custom tag names, existing case variants, and deterministic duplicates", async () => { const { adapter, calls } = fakeAdapter(note(false), [ { id: "z", title: "FIRE" }, { id: "a", title: "fire" }, { id: "keep", title: "keep-me" }, ], [{ id: "m", title: "MATTERS" }]); await moveMatrixItem(adapter, "card", "topRight", { ...config, urgentTag: "fire", importantTag: "matters", }); expect(calls).toEqual(["detach:a", "detach:z", "attach:m"]); }); test("creates only a missing required tag", async () => { const { adapter, calls } = fakeAdapter(note(true), [], []); await moveMatrixItem(adapter, "card", "bottomLeft", config); expect(calls).toEqual(["create:urgent", "attach:urgent"]); }); test.each(["skeleton", "eisenhower"] as const)( "%s Done completes a native to-do and removes its in-progress tag", async (mode) => { const { adapter, calls } = fakeAdapter(note(true), [ { id: "p", title: "in-progress" }, { id: "u", title: "urgent" }, ]); await moveMatrixItem(adapter, "card", "done", { ...config, mode }); expect(calls).toEqual(["complete:true", "detach:p"]); } ); test.each(["skeleton", "eisenhower"] as const)( "%s Done applies the configured done tag to an ordinary note", async (mode) => { const { adapter, calls } = fakeAdapter(note(false), [ { id: "p", title: "in-progress" }, ], [{ id: "d", title: "Finished" }]); await moveMatrixItem(adapter, "card", "done", { ...config, mode, doneTag: "finished", }); expect(calls).toEqual(["attach:d", "detach:p"]); } ); test.each([ ["topLeft", "urgent"], ["topRight", "in-progress"], ["bottomLeft", "on-deck"], ["bottomRight", null], ] as Array<[MatrixDestination, string | null]>) ("Skeleton %s normalizes the three workflow tags", async (destination, expected) => { const { adapter, calls } = fakeAdapter(note(false), [ { id: "u", title: "urgent" }, { id: "i", title: "in-progress" }, { id: "o", title: "on-deck" }, ], [ { id: "u", title: "urgent" }, { id: "i", title: "in-progress" }, { id: "o", title: "on-deck" }, ]); await moveMatrixItem(adapter, "card", destination, { ...config, mode: "skeleton" }); const keptId = expected === "urgent" ? "u" : expected === "in-progress" ? "i" : expected === "on-deck" ? "o" : null; expect(calls).toEqual(["u", "i", "o"].filter((id) => id !== keptId).map((id) => `detach:${id}`)); }); test("fresh validation can reject completed or newly filtered cards before writes", async () => { for (const current of [note(true), note(false)]) { current.todo_completed = current.is_todo ? 100 : 0; const { adapter, calls } = fakeAdapter(current, []); await expect(moveMatrixItem(adapter, "card", "topLeft", config, () => { throw new Error("card is no longer eligible"); })).rejects.toThrow("no longer eligible"); expect(calls).toEqual([]); } }); test("surfaces partial failures without compensating writes", async () => { const { adapter, calls } = fakeAdapter(note(false), [], [ { id: "u", title: "urgent" }, { id: "i", title: "important" }, ]); adapter.attachTag = async (tagId) => { calls.push(`attach:${tagId}`); if (tagId === "i") throw new Error("important attach failed"); }; await expect(moveMatrixItem(adapter, "card", "topLeft", config)).rejects.toThrow( "important attach failed" ); expect(calls).toEqual(["attach:u", "attach:i"]); }); test("fails before writes when the card disappeared", async () => { const { adapter, calls } = fakeAdapter(null, []); await expect(moveMatrixItem(adapter, "card", "topLeft", config)).rejects.toThrow( "no longer exists" ); expect(calls).toEqual([]); }); });