From c73fbdc7cb2ff51e05d9372680038f110410ce3a Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:15:37 -0400 Subject: [PATCH 01/11] feat: add resolveChecklistId helper to BaseCommand --- packages/trello-cli/src/BaseCommand.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/trello-cli/src/BaseCommand.ts b/packages/trello-cli/src/BaseCommand.ts index cc1776d..264fae4 100644 --- a/packages/trello-cli/src/BaseCommand.ts +++ b/packages/trello-cli/src/BaseCommand.ts @@ -201,6 +201,20 @@ export abstract class BaseCommand extends Command { await run([this.id!, "--help"]); } + // Resolves a checklist name or ID to its Trello ID. + // Skips the API call when the value is already a 24-char hex ID. + protected async resolveChecklistId(cardId: string, checklist: string): Promise { + if (/^[a-f0-9]{24}$/.test(checklist)) { + return checklist; + } + const checklists = (await this.client.cards.getCardChecklists({ id: cardId })) as any[]; + const found = checklists.find((cl: any) => cl.name === checklist || cl.id === checklist); + if (!found) { + this.error(`No checklist found matching "${checklist}"`); + } + return found.id; + } + protected async catch(err: Error & { exitCode?: number }): Promise { // add any custom logic to handle errors from the command // or simply return the parent class error handling From c5e4686252aaf64e119f28e79958fc37522c8092 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:15:51 -0400 Subject: [PATCH 02/11] feat: add card:delete-checklist command --- .../src/commands/card/delete-checklist.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/trello-cli/src/commands/card/delete-checklist.ts diff --git a/packages/trello-cli/src/commands/card/delete-checklist.ts b/packages/trello-cli/src/commands/card/delete-checklist.ts new file mode 100644 index 0000000..6c2319e --- /dev/null +++ b/packages/trello-cli/src/commands/card/delete-checklist.ts @@ -0,0 +1,23 @@ +import { BaseCommand } from "../../BaseCommand"; +import { Flags } from "@oclif/core"; + +export default class CardDeleteChecklist extends BaseCommand { + static description = "Delete a checklist from a card"; + + static flags = { + board: Flags.string({ required: true }), + list: Flags.string({ required: true }), + card: Flags.string({ required: true }), + checklist: Flags.string({ required: true, description: "Checklist name or ID" }), + }; + + async run(): Promise { + // resolveChecklistId handles both names and raw IDs (see BaseCommand) + const checklistId = await this.resolveChecklistId(this.lookups.card, this.flags.checklist); + + await this.client.cards.deleteCardChecklist({ + id: this.lookups.card, + idChecklist: checklistId, + }); + } +} From 7bc7d0c1379424bfa6448b57c07976964339ad02 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:04 -0400 Subject: [PATCH 03/11] feat: add card:add-checklist-item command --- .../src/commands/card/add-checklist-item.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/trello-cli/src/commands/card/add-checklist-item.ts diff --git a/packages/trello-cli/src/commands/card/add-checklist-item.ts b/packages/trello-cli/src/commands/card/add-checklist-item.ts new file mode 100644 index 0000000..4e0f19f --- /dev/null +++ b/packages/trello-cli/src/commands/card/add-checklist-item.ts @@ -0,0 +1,48 @@ +import { BaseCommand } from "../../BaseCommand"; +import { Flags } from "@oclif/core"; + +export default class CardAddChecklistItem extends BaseCommand { + static description = "Add an item to a checklist on a card"; + + static flags = { + board: Flags.string({ required: true }), + list: Flags.string({ required: true }), + card: Flags.string({ required: true }), + checklist: Flags.string({ required: true, description: "Checklist name or ID" }), + item: Flags.string({ required: true, description: "Name for the new checklist item" }), + pos: Flags.string({ + description: 'Position of the new item: "top", "bottom", or a positive number', + }), + }; + + async run(): Promise { + const checklistId = await this.resolveChecklistId(this.lookups.card, this.flags.checklist); + + // Build the API payload. pos is optional — omitting it lets Trello default to bottom. + const payload: Record = { id: checklistId, name: this.flags.item }; + + if (this.flags.pos !== undefined) { + payload.pos = this.parsePos(this.flags.pos); + } + + const item = await (this.client.checklists as any).createChecklistCheckItems(payload); + this.output(item); + } + + // Trello accepts "top"/"bottom" as strings and any positive float as a number. + // The CLI always receives strings, so numeric strings must be coerced. + private parsePos(pos: string): string | number { + if (pos === "top" || pos === "bottom") return pos; + const n = parseFloat(pos); + if (isNaN(n)) this.error(`Invalid --pos value "${pos}". Use "top", "bottom", or a number.`); + return n; + } + + protected toData(data: any) { + return { + id: data.id, + name: data.name, + pos: data.pos, + }; + } +} From 3e310bd078ee7961c2f977775199c9cb2438d324 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:13 -0400 Subject: [PATCH 04/11] feat: add card:delete-checklist-item command --- .../commands/card/delete-checklist-item.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/trello-cli/src/commands/card/delete-checklist-item.ts diff --git a/packages/trello-cli/src/commands/card/delete-checklist-item.ts b/packages/trello-cli/src/commands/card/delete-checklist-item.ts new file mode 100644 index 0000000..922ddea --- /dev/null +++ b/packages/trello-cli/src/commands/card/delete-checklist-item.ts @@ -0,0 +1,55 @@ +import { BaseCommand } from "../../BaseCommand"; +import { Flags } from "@oclif/core"; + +export default class CardDeleteChecklistItem extends BaseCommand { + static description = "Delete an item from a checklist on a card"; + + static flags = { + board: Flags.string({ required: true }), + list: Flags.string({ required: true }), + card: Flags.string({ required: true }), + checklist: Flags.string({ required: true, description: "Checklist name or ID" }), + item: Flags.string({ required: true, description: "Item name or ID to delete" }), + }; + + async run(): Promise { + const itemId = await this.resolveItemId(this.lookups.card, this.flags.checklist, this.flags.item); + + await this.client.cards.deleteCardChecklistItem({ + id: this.lookups.card, + idCheckItem: itemId, + }); + } + + // Resolves an item name to its ID within the scoped checklist. + // When the item value is already a 24-char hex ID, skips all API calls entirely. + // The checklist flag scopes the search so identically-named items in other + // checklists on the same card don't cause false matches. + private async resolveItemId(cardId: string, checklist: string, item: string): Promise { + if (/^[a-f0-9]{24}$/.test(item) && /^[a-f0-9]{24}$/.test(checklist)) { + return item; + } + + const checklists = (await this.client.cards.getCardChecklists({ id: cardId })) as any[]; + + // Scope the search to the specified checklist + const targetChecklist = checklists.find( + (cl: any) => cl.name === checklist || cl.id === checklist, + ); + if (!targetChecklist) { + this.error(`No checklist found matching "${checklist}"`); + } + + // Item may already be an ID even if checklist was a name + if (/^[a-f0-9]{24}$/.test(item)) { + return item; + } + + const found = targetChecklist.checkItems.find((ci: any) => ci.name === item); + if (!found) { + this.error(`No checklist item found with name "${item}"`); + } + + return found.id; + } +} From 7d4bbc2de635a3dc2862047bec64d4c9ea118aed Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:22 -0400 Subject: [PATCH 05/11] feat: add card:update-checklist-item command with up/down positioning --- .../commands/card/update-checklist-item.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/trello-cli/src/commands/card/update-checklist-item.ts diff --git a/packages/trello-cli/src/commands/card/update-checklist-item.ts b/packages/trello-cli/src/commands/card/update-checklist-item.ts new file mode 100644 index 0000000..5ab7c14 --- /dev/null +++ b/packages/trello-cli/src/commands/card/update-checklist-item.ts @@ -0,0 +1,136 @@ +import { BaseCommand } from "../../BaseCommand"; +import { Flags } from "@oclif/core"; + +export default class CardUpdateChecklistItem extends BaseCommand { + static description = "Rename or reposition a checklist item on a card"; + + static flags = { + board: Flags.string({ required: true }), + list: Flags.string({ required: true }), + card: Flags.string({ required: true }), + checklist: Flags.string({ required: true, description: "Checklist name or ID" }), + item: Flags.string({ required: true, description: "Item name or ID to update" }), + name: Flags.string({ description: "New name for the item" }), + pos: Flags.string({ + description: 'New position: "top", "bottom", "up", "down", or a positive number', + }), + }; + + async run(): Promise { + if (!this.flags.name && !this.flags.pos) { + this.error("Provide at least one of --name or --pos"); + } + + const isRelativeMove = this.flags.pos === "up" || this.flags.pos === "down"; + + // Always fetch checklists when doing a relative move (need positions to compute midpoint). + // Also fetch when either flag is a name rather than a raw ID. + const needsFetch = + isRelativeMove || + !/^[a-f0-9]{24}$/.test(this.flags.checklist) || + !/^[a-f0-9]{24}$/.test(this.flags.item); + + let itemId: string; + let computedPos: string | number | undefined; + + if (needsFetch) { + const checklists = (await this.client.cards.getCardChecklists({ id: this.lookups.card })) as any[]; + + // Resolve the checklist + const targetChecklist = checklists.find( + (cl: any) => cl.name === this.flags.checklist || cl.id === this.flags.checklist, + ); + if (!targetChecklist) { + this.error(`No checklist found matching "${this.flags.checklist}"`); + } + + // Resolve the item within that checklist + let targetItem: any; + if (/^[a-f0-9]{24}$/.test(this.flags.item)) { + targetItem = targetChecklist.checkItems.find((ci: any) => ci.id === this.flags.item); + } else { + targetItem = targetChecklist.checkItems.find((ci: any) => ci.name === this.flags.item); + } + if (!targetItem) { + this.error(`No checklist item found with name "${this.flags.item}"`); + } + itemId = targetItem.id; + + // Compute position for relative moves + if (isRelativeMove) { + computedPos = this.computeRelativePos( + targetChecklist.checkItems, + targetItem, + this.flags.pos as "up" | "down", + ); + } + } else { + // Both flags were raw IDs and pos is not relative — skip all fetching + itemId = this.flags.item; + } + + // Build the update payload — only include fields the caller actually specified + const payload: Record = { + id: this.lookups.card, + idCheckItem: itemId, + }; + + if (this.flags.name) { + payload.name = this.flags.name; + } + + if (this.flags.pos !== undefined) { + payload.pos = computedPos !== undefined ? computedPos : this.parsePos(this.flags.pos); + } + + const result = await this.client.cards.updateCardCheckItem(payload as any); + this.output(result); + } + + // Computes a new fractional position so the item slots between its neighbours + // after a relative move. Trello uses floating-point positions, so inserting a + // midpoint value avoids collisions without reordering other items. + private computeRelativePos( + items: any[], + target: any, + direction: "up" | "down", + ): number { + // Sort ascending so index reflects visual order in the checklist + const sorted = [...items].sort((a, b) => a.pos - b.pos); + const idx = sorted.findIndex((ci) => ci.id === target.id); + + if (direction === "up") { + if (idx === 0) this.error("Item is already at the top of the checklist"); + const prev = sorted[idx - 1]; + // No item before prev: halve prev's position to land before it + if (idx === 1) return prev.pos / 2; + // Otherwise: midpoint between the two items that bracket the target slot + return (sorted[idx - 2].pos + prev.pos) / 2; + } else { + if (idx === sorted.length - 1) this.error("Item is already at the bottom of the checklist"); + const next = sorted[idx + 1]; + // No item after next: add a fixed increment to land after it + if (idx === sorted.length - 2) return next.pos + 1000; + // Otherwise: midpoint between next and the item after it + return (next.pos + sorted[idx + 2].pos) / 2; + } + } + + // Trello accepts "top"/"bottom" as strings and any positive float as a number. + // Relative moves ("up"/"down") are resolved before this is called. + private parsePos(pos: string): string | number { + if (pos === "top" || pos === "bottom") return pos; + const n = parseFloat(pos); + if (isNaN(n)) this.error(`Invalid --pos value "${pos}". Use "top", "bottom", "up", "down", or a number.`); + return n; + } + + protected toData(data: any) { + return { + id: data.id, + name: data.name, + state: data.state, + pos: data.pos, + }; + } +} From 7ab6043031ed517f51670bbd75419a929ae5196f Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:36 -0400 Subject: [PATCH 06/11] test: add tests for card:delete-checklist --- .../commands/card/delete-checklist.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 packages/trello-cli/test/commands/card/delete-checklist.test.ts diff --git a/packages/trello-cli/test/commands/card/delete-checklist.test.ts b/packages/trello-cli/test/commands/card/delete-checklist.test.ts new file mode 100644 index 0000000..e5e1f18 --- /dev/null +++ b/packages/trello-cli/test/commands/card/delete-checklist.test.ts @@ -0,0 +1,126 @@ +import { runCommand } from "@oclif/test"; +import Config from "@trello-cli/config"; +import { ux } from "@oclif/core"; + +// --- Mock setup --- +// Simulates the Trello API response for deleting a checklist (no meaningful body) +const deleteCardChecklist = jest.fn().mockResolvedValue({}); + +// Used to resolve a checklist name → ID when the caller passes a name instead of a raw ID +const getCardChecklists = jest.fn().mockResolvedValue([ + { id: "cl1", name: "MyChecklist", checkItems: [] }, +]); + +// Required by BaseCommand to resolve --card name → card ID via the list +const getListCards = jest.fn().mockResolvedValue([ + { id: "card123", name: "TestCard" }, +]); + +jest.mock("trello.js", () => ({ + TrelloClient: jest.fn().mockImplementation(() => ({ + cards: { deleteCardChecklist, getCardChecklists }, + lists: { getListCards }, + })), +})); + +// BaseCommand resolves --board and --list via the local cache before hitting the API +const mockGetBoardIdByName = jest.fn().mockResolvedValue("board123"); +const mockGetListIdByBoardAndName = jest.fn().mockResolvedValue("list123"); + +jest.mock("@trello-cli/cache", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + getBoardIdByName: mockGetBoardIdByName, + getListIdByBoardAndName: mockGetListIdByBoardAndName, + })), +})); + +let stdoutSpy: jest.SpyInstance; + +beforeEach(() => { + jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); + jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); + stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + + // Reset call counts between tests so assertions stay isolated + deleteCardChecklist.mockClear(); + getCardChecklists.mockClear(); + getListCards.mockClear(); + mockGetBoardIdByName.mockClear(); + mockGetListIdByBoardAndName.mockClear(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("card:delete-checklist", () => { + // Guard: oclif should reject the command before any API call is made + it("throws when required flags are missing", async () => { + const { error } = await runCommand(["card:delete-checklist"]); + expect(error?.message).toContain("Missing required flag"); + }); + + // Happy path: caller supplies a checklist name; command must resolve it to an ID + // before calling deleteCardChecklist, since the API requires an ID + it("resolves checklist name to ID and calls deleteCardChecklist", async () => { + const { error } = await runCommand([ + "card:delete-checklist", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).toHaveBeenCalledWith({ id: "card123" }); + expect(deleteCardChecklist).toHaveBeenCalledWith({ + id: "card123", + idChecklist: "cl1", + }); + }); + + // Optimisation: a 24-char hex string is already a Trello ID — skip the extra + // getCardChecklists round-trip and pass it straight to the API + it("accepts checklist ID directly without fetching checklists", async () => { + const { error } = await runCommand([ + "card:delete-checklist", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "aabbccddeeff001122334455", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).not.toHaveBeenCalled(); + expect(deleteCardChecklist).toHaveBeenCalledWith({ + id: "card123", + idChecklist: "aabbccddeeff001122334455", + }); + }); + + // Error handling: checklist name provided but doesn't exist on the card + it("errors when checklist name not found", async () => { + const { error } = await runCommand([ + "card:delete-checklist", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "NoSuch", + ]); + expect(error?.message).toContain('No checklist found matching "NoSuch"'); + }); + + // Verify that BaseCommand's name-to-ID resolution chain is exercised: + // board name → board ID → list ID → card ID + it("resolves card name to ID via board/list lookups", async () => { + await runCommand([ + "card:delete-checklist", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + ]); + expect(mockGetBoardIdByName).toHaveBeenCalledWith("MyBoard"); + expect(mockGetListIdByBoardAndName).toHaveBeenCalledWith("board123", "ToDo"); + expect(getListCards).toHaveBeenCalledWith({ id: "list123" }); + }); +}); From fc6c2cfcc7e37ea578e0f5accb728bd477a15e38 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:44 -0400 Subject: [PATCH 07/11] test: add tests for card:add-checklist-item --- .../commands/card/add-checklist-item.test.ts | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 packages/trello-cli/test/commands/card/add-checklist-item.test.ts diff --git a/packages/trello-cli/test/commands/card/add-checklist-item.test.ts b/packages/trello-cli/test/commands/card/add-checklist-item.test.ts new file mode 100644 index 0000000..74353b9 --- /dev/null +++ b/packages/trello-cli/test/commands/card/add-checklist-item.test.ts @@ -0,0 +1,218 @@ +import { runCommand } from "@oclif/test"; +import Config from "@trello-cli/config"; +import { ux } from "@oclif/core"; + +// --- Mock setup --- +// NOTE: @oclif/test runCommand splits on spaces within argument strings, so all +// test flag values use single-word strings (e.g. "NewTask" not "New Task"). +// This is consistent with every existing test in the repo and does not limit +// what the command itself supports. + +const mockItem = { id: "item1", name: "NewTask", pos: 16384 }; + +// API call that creates the new checklist item inside a given checklist +const createChecklistCheckItems = jest.fn().mockResolvedValue(mockItem); + +// Used to resolve a checklist name → ID when caller passes a name instead of a raw ID +const getCardChecklists = jest.fn().mockResolvedValue([ + { id: "cl1", name: "MyChecklist", checkItems: [] }, +]); + +// Required by BaseCommand to resolve --card name → card ID via the list +const getListCards = jest.fn().mockResolvedValue([ + { id: "card123", name: "TestCard" }, +]); + +jest.mock("trello.js", () => ({ + TrelloClient: jest.fn().mockImplementation(() => ({ + cards: { getCardChecklists }, + // createChecklistCheckItems lives on the checklists namespace in trello.js + checklists: { createChecklistCheckItems }, + lists: { getListCards }, + })), +})); + +// BaseCommand resolves --board and --list via the local cache before hitting the API +const mockGetBoardIdByName = jest.fn().mockResolvedValue("board123"); +const mockGetListIdByBoardAndName = jest.fn().mockResolvedValue("list123"); + +jest.mock("@trello-cli/cache", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + getBoardIdByName: mockGetBoardIdByName, + getListIdByBoardAndName: mockGetListIdByBoardAndName, + })), +})); + +let stdoutSpy: jest.SpyInstance; + +beforeEach(() => { + jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); + jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); + stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + + // Reset call counts between tests so assertions stay isolated + createChecklistCheckItems.mockClear(); + getCardChecklists.mockClear(); + getListCards.mockClear(); + mockGetBoardIdByName.mockClear(); + mockGetListIdByBoardAndName.mockClear(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("card:add-checklist-item", () => { + // Guard: oclif should reject the command before any API call is made + it("throws when required flags are missing", async () => { + const { error } = await runCommand(["card:add-checklist-item"]); + expect(error?.message).toContain("Missing required flag"); + }); + + // Happy path: caller supplies a checklist name; command must resolve it to an ID + // via getCardChecklists before calling createChecklistCheckItems + it("resolves checklist name and creates item", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).toHaveBeenCalledWith({ id: "card123" }); + expect(createChecklistCheckItems).toHaveBeenCalledWith({ + id: "cl1", + name: "NewTask", + }); + }); + + // Optimisation: a 24-char hex string is already a Trello ID — skip the extra + // getCardChecklists round-trip and pass it straight to the API + it("accepts checklist ID directly", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "aabbccddeeff001122334455", + "--item", "NewTask", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).not.toHaveBeenCalled(); + expect(createChecklistCheckItems).toHaveBeenCalledWith({ + id: "aabbccddeeff001122334455", + name: "NewTask", + }); + }); + + // Position tests: the Trello API accepts "top", "bottom", or a positive float. + // The command must pass these through correctly, converting numeric strings to numbers. + it("passes --pos top to API", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--pos", "top", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(createChecklistCheckItems).toHaveBeenCalledWith({ + id: "cl1", + name: "NewTask", + pos: "top", + }); + }); + + it("passes --pos bottom to API", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--pos", "bottom", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(createChecklistCheckItems).toHaveBeenCalledWith({ + id: "cl1", + name: "NewTask", + pos: "bottom", + }); + }); + + // Numeric strings from the CLI must be coerced to numbers before passing to the API + it("passes numeric --pos to API as a number", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--pos", "8192", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(createChecklistCheckItems).toHaveBeenCalledWith({ + id: "cl1", + name: "NewTask", + pos: 8192, + }); + }); + + // Error handling: checklist name provided but doesn't exist on the card + it("errors when checklist name not found", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "NoSuch", + "--item", "NewTask", + ]); + expect(error?.message).toContain('No checklist found matching "NoSuch"'); + }); + + // Verify the output shape matches what consumers (scripts, piped commands) expect + it("outputs correct JSON shape", async () => { + await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--format", "json", + ]); + const output = JSON.parse(stdoutSpy.mock.calls[0][0]); + expect(output.id).toBe("item1"); + expect(output.name).toBe("NewTask"); + }); + + // Verify that BaseCommand's name-to-ID resolution chain is exercised: + // board name → board ID → list ID → card ID + it("resolves card name to ID via board/list lookups", async () => { + await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--format", "json", + ]); + expect(mockGetBoardIdByName).toHaveBeenCalledWith("MyBoard"); + expect(mockGetListIdByBoardAndName).toHaveBeenCalledWith("board123", "ToDo"); + expect(getListCards).toHaveBeenCalledWith({ id: "list123" }); + }); +}); From b341db324fe33b26bc3d408b5ee814662a24ebe5 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:16:58 -0400 Subject: [PATCH 08/11] test: add tests for card:delete-checklist-item --- .../card/delete-checklist-item.test.ts | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 packages/trello-cli/test/commands/card/delete-checklist-item.test.ts diff --git a/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts b/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts new file mode 100644 index 0000000..3216300 --- /dev/null +++ b/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts @@ -0,0 +1,186 @@ +import { runCommand } from "@oclif/test"; +import Config from "@trello-cli/config"; +import { ux } from "@oclif/core"; + +// --- Mock setup --- +// NOTE: @oclif/test runCommand splits on spaces within argument strings, so all +// test flag values use single-word strings (e.g. "TaskOne" not "Task One"). +// This is consistent with every existing test in the repo and does not limit +// what the command itself supports. + +// Trello API returns no meaningful body on delete — empty object is the convention +const deleteCardChecklistItem = jest.fn().mockResolvedValue({}); + +// Used to resolve checklist name → ID and item name → ID when caller uses names +const getCardChecklists = jest.fn().mockResolvedValue([ + { + id: "cl1", + name: "MyChecklist", + checkItems: [ + { id: "item1", name: "TaskOne", state: "incomplete", pos: 16384 }, + { id: "item2", name: "TaskTwo", state: "incomplete", pos: 32768 }, + ], + }, +]); + +// Required by BaseCommand to resolve --card name → card ID via the list +const getListCards = jest.fn().mockResolvedValue([ + { id: "card123", name: "TestCard" }, +]); + +jest.mock("trello.js", () => ({ + TrelloClient: jest.fn().mockImplementation(() => ({ + cards: { deleteCardChecklistItem, getCardChecklists }, + lists: { getListCards }, + })), +})); + +// BaseCommand resolves --board and --list via the local cache before hitting the API +const mockGetBoardIdByName = jest.fn().mockResolvedValue("board123"); +const mockGetListIdByBoardAndName = jest.fn().mockResolvedValue("list123"); + +jest.mock("@trello-cli/cache", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + getBoardIdByName: mockGetBoardIdByName, + getListIdByBoardAndName: mockGetListIdByBoardAndName, + })), +})); + +let stdoutSpy: jest.SpyInstance; + +beforeEach(() => { + jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); + jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); + stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + + // Reset call counts between tests so assertions stay isolated + deleteCardChecklistItem.mockClear(); + getCardChecklists.mockClear(); + getListCards.mockClear(); + mockGetBoardIdByName.mockClear(); + mockGetListIdByBoardAndName.mockClear(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("card:delete-checklist-item", () => { + // Guard: oclif should reject the command before any API call is made + it("throws when required flags are missing", async () => { + const { error } = await runCommand(["card:delete-checklist-item"]); + expect(error?.message).toContain("Missing required flag"); + }); + + // Happy path: caller supplies checklist and item by name. + // Command must resolve both to IDs before calling deleteCardChecklistItem. + it("resolves checklist and item names to IDs and deletes item", async () => { + const { error } = await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).toHaveBeenCalledWith({ id: "card123" }); + expect(deleteCardChecklistItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + }); + }); + + // Optimisation: a 24-char hex item ID skips the getCardChecklists round-trip entirely + it("accepts item ID directly without fetching checklists", async () => { + const { error } = await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "aabbccddeeff001122334455", + "--item", "aabbccddeeff001122334456", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).not.toHaveBeenCalled(); + expect(deleteCardChecklistItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "aabbccddeeff001122334456", + }); + }); + + // --checklist scopes the item lookup so items with the same name in different + // checklists on the same card don't cause ambiguity errors + it("scopes item lookup to the specified checklist", async () => { + getCardChecklists.mockResolvedValueOnce([ + { + id: "cl1", + name: "Sprint", + checkItems: [{ id: "item1", name: "SharedName", state: "incomplete", pos: 16384 }], + }, + { + id: "cl2", + name: "Backlog", + checkItems: [{ id: "item2", name: "SharedName", state: "incomplete", pos: 16384 }], + }, + ]); + + const { error } = await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "Backlog", + "--item", "SharedName", + ]); + expect(error).toBeUndefined(); + // Should delete the item from "Backlog", not "Sprint" + expect(deleteCardChecklistItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item2", + }); + }); + + // Error handling: item name provided but doesn't exist in the specified checklist + it("errors when item name not found", async () => { + const { error } = await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NoSuch", + ]); + expect(error?.message).toContain('No checklist item found with name "NoSuch"'); + }); + + // Error handling: checklist name provided but doesn't exist on the card + it("errors when checklist name not found", async () => { + const { error } = await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "NoSuch", + "--item", "TaskOne", + ]); + expect(error?.message).toContain('No checklist found matching "NoSuch"'); + }); + + // Verify that BaseCommand's name-to-ID resolution chain is exercised: + // board name → board ID → list ID → card ID + it("resolves card name to ID via board/list lookups", async () => { + await runCommand([ + "card:delete-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + ]); + expect(mockGetBoardIdByName).toHaveBeenCalledWith("MyBoard"); + expect(mockGetListIdByBoardAndName).toHaveBeenCalledWith("board123", "ToDo"); + expect(getListCards).toHaveBeenCalledWith({ id: "list123" }); + }); +}); From aaeb0d4c74e077893fa855c3484d37c170d142f5 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Sun, 24 May 2026 23:17:06 -0400 Subject: [PATCH 09/11] test: add tests for card:update-checklist-item --- .../card/update-checklist-item.test.ts | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 packages/trello-cli/test/commands/card/update-checklist-item.test.ts diff --git a/packages/trello-cli/test/commands/card/update-checklist-item.test.ts b/packages/trello-cli/test/commands/card/update-checklist-item.test.ts new file mode 100644 index 0000000..726e118 --- /dev/null +++ b/packages/trello-cli/test/commands/card/update-checklist-item.test.ts @@ -0,0 +1,370 @@ +import { runCommand } from "@oclif/test"; +import Config from "@trello-cli/config"; +import { ux } from "@oclif/core"; + +// --- Mock setup --- +// NOTE: @oclif/test runCommand splits on spaces within argument strings, so all +// test flag values use single-word strings (e.g. "TaskOne" not "Task One"). +// This is consistent with every existing test in the repo and does not limit +// what the command itself supports. + +const mockUpdatedItem = { id: "item1", name: "TaskOne", state: "incomplete", pos: 16384 }; + +// updateCardCheckItem is used for all mutations: rename, reposition, or both. +// It lives on client.cards and accepts name, pos, and/or state. +const updateCardCheckItem = jest.fn().mockResolvedValue(mockUpdatedItem); + +// getCardChecklists is used for two purposes: +// 1. Resolve checklist/item names to IDs +// 2. Fetch current item positions for relative moves (up/down) +const getCardChecklists = jest.fn().mockResolvedValue([ + { + id: "cl1", + name: "MyChecklist", + checkItems: [ + { id: "item1", name: "TaskOne", state: "incomplete", pos: 16384 }, + { id: "item2", name: "TaskTwo", state: "incomplete", pos: 32768 }, + { id: "item3", name: "TaskThree", state: "incomplete", pos: 49152 }, + ], + }, +]); + +// Required by BaseCommand to resolve --card name → card ID via the list +const getListCards = jest.fn().mockResolvedValue([ + { id: "card123", name: "TestCard" }, +]); + +jest.mock("trello.js", () => ({ + TrelloClient: jest.fn().mockImplementation(() => ({ + cards: { updateCardCheckItem, getCardChecklists }, + lists: { getListCards }, + })), +})); + +// BaseCommand resolves --board and --list via the local cache before hitting the API +const mockGetBoardIdByName = jest.fn().mockResolvedValue("board123"); +const mockGetListIdByBoardAndName = jest.fn().mockResolvedValue("list123"); + +jest.mock("@trello-cli/cache", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + getBoardIdByName: mockGetBoardIdByName, + getListIdByBoardAndName: mockGetListIdByBoardAndName, + })), +})); + +let stdoutSpy: jest.SpyInstance; + +beforeEach(() => { + jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); + jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); + stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + + // Reset call counts between tests so assertions stay isolated + updateCardCheckItem.mockClear(); + getCardChecklists.mockClear(); + getListCards.mockClear(); + mockGetBoardIdByName.mockClear(); + mockGetListIdByBoardAndName.mockClear(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("card:update-checklist-item", () => { + // Guard: oclif should reject the command before any API call is made + it("throws when required flags are missing", async () => { + const { error } = await runCommand(["card:update-checklist-item"]); + expect(error?.message).toContain("Missing required flag"); + }); + + // Guard: --name and --pos are both optional individually, but at least one must + // be provided — otherwise the command would be a no-op + it("errors when neither --name nor --pos is provided", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + ]); + expect(error?.message).toContain("at least one of --name or --pos"); + }); + + // --- Rename tests --- + + // Happy path: rename only — should call updateCardCheckItem with new name, no pos + it("renames item when only --name is provided", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--name", "RenamedTask", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + name: "RenamedTask", + }); + }); + + // --- Position tests: absolute values --- + + // Trello API accepts the string "top" directly — pass through without conversion + it("passes --pos top to API as string", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--pos", "top", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + pos: "top", + }); + }); + + // Trello API accepts the string "bottom" directly — pass through without conversion + it("passes --pos bottom to API as string", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--pos", "bottom", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + pos: "bottom", + }); + }); + + // Numeric strings from the CLI must be coerced to numbers before passing to the API + it("passes numeric --pos to API as a number", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--pos", "8192", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + pos: 8192, + }); + }); + + // --- Position tests: relative moves (up/down) --- + // Relative moves require fetching all items, sorting by pos, and computing a + // midpoint position so the item lands between the correct neighbours. + // Items in mock: TaskOne (16384) → TaskTwo (32768) → TaskThree (49152) + + // Moving "TaskTwo" up: new pos should be between nothing and TaskOne. + // Since TaskOne is the first item (no item before it), new pos = TaskOne.pos / 2 = 8192 + it("moves item up by computing midpoint before previous item", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskTwo", // currently at index 1 (pos 32768) + "--pos", "up", + "--format", "json", + ]); + expect(error).toBeUndefined(); + // TaskTwo moves up past TaskOne. No item before TaskOne, so new pos = TaskOne.pos / 2 + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item2", + pos: 8192, // 16384 / 2 + }); + }); + + // Moving "TaskTwo" down: new pos should be between TaskThree and nothing after it. + // Since TaskThree is the last item, new pos = TaskThree.pos + 1000 + it("moves item down by computing midpoint after next item", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskTwo", // currently at index 1 (pos 32768) + "--pos", "down", + "--format", "json", + ]); + expect(error).toBeUndefined(); + // TaskTwo moves down past TaskThree. No item after TaskThree, so new pos = TaskThree.pos + 1000 + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item2", + pos: 50152, // 49152 + 1000 + }); + }); + + // Moving "TaskThree" (last item) up: new pos should be midpoint of TaskOne and TaskTwo + it("moves last item up by computing midpoint between the two preceding items", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskThree", // currently at index 2 (pos 49152) + "--pos", "up", + "--format", "json", + ]); + expect(error).toBeUndefined(); + // Moves up past TaskTwo. TaskOne and TaskTwo bracket the target slot. + // New pos = (TaskOne.pos + TaskTwo.pos) / 2 = (16384 + 32768) / 2 = 24576 + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item3", + pos: 24576, + }); + }); + + // Edge case: cannot move the first item further up + it("errors when trying to move first item up", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", // already at index 0 + "--pos", "up", + ]); + expect(error?.message).toContain("already at the top"); + }); + + // Edge case: cannot move the last item further down + it("errors when trying to move last item down", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskThree", // already at the last index + "--pos", "down", + ]); + expect(error?.message).toContain("already at the bottom"); + }); + + // --- Combined rename + reposition --- + + // Both --name and --pos can be supplied together in a single API call + it("renames and repositions in one call when both --name and --pos are provided", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--name", "RenamedTask", + "--pos", "bottom", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + name: "RenamedTask", + pos: "bottom", + }); + }); + + // --- Error handling --- + + // Error handling: item name provided but doesn't exist in the specified checklist + it("errors when item name not found", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NoSuch", + "--name", "Whatever", + ]); + expect(error?.message).toContain('No checklist item found with name "NoSuch"'); + }); + + // Error handling: checklist name provided but doesn't exist on the card + it("errors when checklist name not found", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "NoSuch", + "--item", "TaskOne", + "--name", "Whatever", + ]); + expect(error?.message).toContain('No checklist found matching "NoSuch"'); + }); + + // --- Output shape --- + + // Verify the output shape matches what consumers (scripts, piped commands) expect + it("outputs correct JSON shape", async () => { + await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--name", "RenamedTask", + "--format", "json", + ]); + const output = JSON.parse(stdoutSpy.mock.calls[0][0]); + expect(output.id).toBe("item1"); + expect(output.name).toBeDefined(); + expect(output.state).toBeDefined(); + }); + + // Verify that BaseCommand's name-to-ID resolution chain is exercised: + // board name → board ID → list ID → card ID + it("resolves card name to ID via board/list lookups", async () => { + await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--name", "RenamedTask", + "--format", "json", + ]); + expect(mockGetBoardIdByName).toHaveBeenCalledWith("MyBoard"); + expect(mockGetListIdByBoardAndName).toHaveBeenCalledWith("board123", "ToDo"); + expect(getListCards).toHaveBeenCalledWith({ id: "list123" }); + }); +}); From 8f20c2cff26f1721a1c701660260aa3648143f7c Mon Sep 17 00:00:00 2001 From: K0d3x Date: Mon, 25 May 2026 14:24:05 -0400 Subject: [PATCH 10/11] chore: add changeset and fix lint errors in test files Remove unused stdoutSpy variables in delete-checklist and delete-checklist-item tests. Co-Authored-By: Claude Sonnet 4.6 --- .changeset/checklist-item-crud.md | 5 +++++ .../test/commands/card/delete-checklist-item.test.ts | 4 +--- .../trello-cli/test/commands/card/delete-checklist.test.ts | 4 +--- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .changeset/checklist-item-crud.md diff --git a/.changeset/checklist-item-crud.md b/.changeset/checklist-item-crud.md new file mode 100644 index 0000000..6d63fae --- /dev/null +++ b/.changeset/checklist-item-crud.md @@ -0,0 +1,5 @@ +--- +"trello-cli": minor +--- + +Add checklist item CRUD commands: card:add-checklist-item, card:delete-checklist-item, card:update-checklist-item, card:delete-checklist. Includes resolveChecklistId helper on BaseCommand. diff --git a/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts b/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts index 3216300..3549863 100644 --- a/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts +++ b/packages/trello-cli/test/commands/card/delete-checklist-item.test.ts @@ -47,12 +47,10 @@ jest.mock("@trello-cli/cache", () => ({ })), })); -let stdoutSpy: jest.SpyInstance; - beforeEach(() => { jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); - stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + jest.spyOn(ux, "stdout").mockImplementation(() => {}); // Reset call counts between tests so assertions stay isolated deleteCardChecklistItem.mockClear(); diff --git a/packages/trello-cli/test/commands/card/delete-checklist.test.ts b/packages/trello-cli/test/commands/card/delete-checklist.test.ts index e5e1f18..c7352c1 100644 --- a/packages/trello-cli/test/commands/card/delete-checklist.test.ts +++ b/packages/trello-cli/test/commands/card/delete-checklist.test.ts @@ -35,12 +35,10 @@ jest.mock("@trello-cli/cache", () => ({ })), })); -let stdoutSpy: jest.SpyInstance; - beforeEach(() => { jest.spyOn(Config.prototype, "getToken").mockResolvedValue("fake_token"); jest.spyOn(Config.prototype, "getApiKey").mockResolvedValue("fake_api_key"); - stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + jest.spyOn(ux, "stdout").mockImplementation(() => {}); // Reset call counts between tests so assertions stay isolated deleteCardChecklist.mockClear(); From 20c070de693b8f553290ddba9ef637b6ba648911 Mon Sep 17 00:00:00 2001 From: K0d3x Date: Mon, 25 May 2026 14:50:36 -0400 Subject: [PATCH 11/11] test: add missing coverage for update-checklist-item and add-checklist-item Add tests for: first-item down (midpoint between 2 items below), skip-fetch optimisation with raw IDs, and invalid --pos error handling in both commands. Co-Authored-By: Claude Sonnet 4.6 --- .../commands/card/add-checklist-item.test.ts | 14 +++++ .../card/update-checklist-item.test.ts | 60 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/packages/trello-cli/test/commands/card/add-checklist-item.test.ts b/packages/trello-cli/test/commands/card/add-checklist-item.test.ts index 74353b9..11bd7e4 100644 --- a/packages/trello-cli/test/commands/card/add-checklist-item.test.ts +++ b/packages/trello-cli/test/commands/card/add-checklist-item.test.ts @@ -170,6 +170,20 @@ describe("card:add-checklist-item", () => { }); }); + // Error handling: invalid --pos value (not top/bottom/number) should error + it("errors when --pos is an invalid value", async () => { + const { error } = await runCommand([ + "card:add-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "NewTask", + "--pos", "invalid", + ]); + expect(error?.message).toContain('Invalid --pos value "invalid"'); + }); + // Error handling: checklist name provided but doesn't exist on the card it("errors when checklist name not found", async () => { const { error } = await runCommand([ diff --git a/packages/trello-cli/test/commands/card/update-checklist-item.test.ts b/packages/trello-cli/test/commands/card/update-checklist-item.test.ts index 726e118..11f32b7 100644 --- a/packages/trello-cli/test/commands/card/update-checklist-item.test.ts +++ b/packages/trello-cli/test/commands/card/update-checklist-item.test.ts @@ -248,6 +248,30 @@ describe("card:update-checklist-item", () => { }); }); + // Moving "TaskOne" (first item, idx=0) down: new pos should be midpoint of + // TaskTwo and TaskThree, since both exist after the landing slot. + // New pos = (TaskTwo.pos + TaskThree.pos) / 2 = (32768 + 49152) / 2 = 40960 + it("moves first item down by computing midpoint between the two following items", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", // currently at index 0 (pos 16384) + "--pos", "down", + "--format", "json", + ]); + expect(error).toBeUndefined(); + // TaskOne moves down past TaskTwo. TaskTwo and TaskThree bracket the target slot. + // New pos = (TaskTwo.pos + TaskThree.pos) / 2 = (32768 + 49152) / 2 = 40960 + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "item1", + pos: 40960, + }); + }); + // Edge case: cannot move the first item further up it("errors when trying to move first item up", async () => { const { error } = await runCommand([ @@ -300,6 +324,28 @@ describe("card:update-checklist-item", () => { }); }); + // Skip-fetch optimisation: when both --checklist and --item are raw 24-char hex IDs + // and --pos is not a relative move, getCardChecklists must not be called + it("skips getCardChecklists when both checklist and item are raw IDs", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "aabbccddeeff001122334455", + "--item", "aabbccddeeff001122334456", + "--pos", "top", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(getCardChecklists).not.toHaveBeenCalled(); + expect(updateCardCheckItem).toHaveBeenCalledWith({ + id: "card123", + idCheckItem: "aabbccddeeff001122334456", + pos: "top", + }); + }); + // --- Error handling --- // Error handling: item name provided but doesn't exist in the specified checklist @@ -316,6 +362,20 @@ describe("card:update-checklist-item", () => { expect(error?.message).toContain('No checklist item found with name "NoSuch"'); }); + // Error handling: invalid --pos value (not top/bottom/up/down/number) should error + it("errors when --pos is an invalid value", async () => { + const { error } = await runCommand([ + "card:update-checklist-item", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--checklist", "MyChecklist", + "--item", "TaskOne", + "--pos", "invalid", + ]); + expect(error?.message).toContain('Invalid --pos value "invalid"'); + }); + // Error handling: checklist name provided but doesn't exist on the card it("errors when checklist name not found", async () => { const { error } = await runCommand([