Skip to content

Commit 38ee35d

Browse files
committed
fix move partial failure reporting, deduplicate formatDate, clean JSON output, add test coverage
1 parent d7ec131 commit 38ee35d

File tree

9 files changed

+100
-17
lines changed

9 files changed

+100
-17
lines changed

src/commands/move.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,19 @@ export async function moveTask(config: Config, taskId: string, opts: MoveOptions
2020
}
2121

2222
if (opts.remove) {
23-
await client.removeTaskFromList(taskId, opts.remove)
24-
messages.push(`Removed ${taskId} from list ${opts.remove}`)
23+
try {
24+
await client.removeTaskFromList(taskId, opts.remove)
25+
messages.push(`Removed ${taskId} from list ${opts.remove}`)
26+
} catch (err) {
27+
if (messages.length > 0) {
28+
const reason = err instanceof Error ? err.message : String(err)
29+
throw new Error(
30+
`${messages.join('; ')}; but failed to remove from list ${opts.remove}: ${reason}`,
31+
{ cause: err },
32+
)
33+
}
34+
throw err
35+
}
2536
}
2637

2738
return messages.join('; ')

src/commands/tasks.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ClickUpClient } from '../api.js'
22
import type { Task, TaskFilters } from '../api.js'
33
import type { Config } from '../config.js'
4+
import { formatDate } from '../date.js'
45
import { isTTY, shouldOutputJson } from '../output.js'
56
import { formatTasksMarkdown } from '../markdown.js'
67
import { interactiveTaskPicker, showDetailsAndOpen } from '../interactive.js'
@@ -35,11 +36,7 @@ function isInitiative(task: Task): boolean {
3536

3637
function formatDueDate(ms: string | null | undefined): string {
3738
if (!ms) return ''
38-
const d = new Date(Number(ms))
39-
const year = d.getUTCFullYear()
40-
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
41-
const day = String(d.getUTCDate()).padStart(2, '0')
42-
return `${year}-${month}-${day}`
39+
return formatDate(ms)
4340
}
4441

4542
export function summarize(task: Task): TaskSummary {

src/date.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function formatDate(ms: string | number): string {
2+
const d = new Date(Number(ms))
3+
const year = d.getUTCFullYear()
4+
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
5+
const day = String(d.getUTCDate()).padStart(2, '0')
6+
return `${year}-${month}-${day}`
7+
}

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,13 @@ program
466466
const config = loadConfig()
467467
const message = await manageDependency(config, taskId, opts)
468468
if (shouldOutputJson(opts.json ?? false)) {
469-
console.log(JSON.stringify({ taskId, ...opts, message }, null, 2))
469+
console.log(
470+
JSON.stringify(
471+
{ taskId, on: opts.on, blocks: opts.blocks, remove: opts.remove, message },
472+
null,
473+
2,
474+
),
475+
)
470476
} else {
471477
console.log(message)
472478
}
@@ -484,7 +490,7 @@ program
484490
const config = loadConfig()
485491
const message = await moveTask(config, taskId, opts)
486492
if (shouldOutputJson(opts.json ?? false)) {
487-
console.log(JSON.stringify({ taskId, ...opts, message }, null, 2))
493+
console.log(JSON.stringify({ taskId, to: opts.to, remove: opts.remove, message }, null, 2))
488494
} else {
489495
console.log(message)
490496
}

src/markdown.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Task } from './api.js'
22
import type { TaskSummary } from './commands/tasks.js'
33
import type { CommentSummary } from './commands/comments.js'
44
import type { ListSummary } from './commands/lists.js'
5+
import { formatDate } from './date.js'
56

67
export interface MarkdownColumn<T> {
78
key: keyof T & string
@@ -73,14 +74,6 @@ export function formatGroupedTasksMarkdown(
7374
return sections.join('\n\n')
7475
}
7576

76-
function formatDate(ms: string): string {
77-
const d = new Date(Number(ms))
78-
const year = d.getUTCFullYear()
79-
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
80-
const day = String(d.getUTCDate()).padStart(2, '0')
81-
return `${year}-${month}-${day}`
82-
}
83-
8477
function formatDuration(ms: number): string {
8578
const totalMinutes = Math.floor(ms / 60000)
8679
const hours = Math.floor(totalMinutes / 60)

tests/unit/commands/inbox.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ describe('fetchInbox', () => {
104104
expect(result).toHaveLength(0)
105105
})
106106

107+
it('passes includeClosed to getMyTasks when set', async () => {
108+
mockGetMyTasks.mockResolvedValue([])
109+
const { fetchInbox } = await import('../../../src/commands/inbox.js')
110+
await fetchInbox({ apiToken: 'pk_t', teamId: 'team1' }, 30, { includeClosed: true })
111+
expect(mockGetMyTasks).toHaveBeenCalledWith('team1', { subtasks: true, includeClosed: true })
112+
})
113+
114+
it('does not pass includeClosed when not set', async () => {
115+
mockGetMyTasks.mockResolvedValue([])
116+
const { fetchInbox } = await import('../../../src/commands/inbox.js')
117+
await fetchInbox({ apiToken: 'pk_t', teamId: 'team1' }, 30)
118+
expect(mockGetMyTasks).toHaveBeenCalledWith('team1', {
119+
subtasks: true,
120+
includeClosed: undefined,
121+
})
122+
})
123+
107124
it('includes date_updated in returned summaries', async () => {
108125
const now = Date.now()
109126
mockGetMyTasks.mockResolvedValue([makeTask('t1', now - 1000)])

tests/unit/commands/move.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ describe('moveTask', () => {
4444
expect(msg).toContain('Removed')
4545
})
4646

47+
it('reports partial success when add succeeds but remove fails', async () => {
48+
mockRemoveTaskFromList.mockRejectedValueOnce(new Error('list not found'))
49+
const { moveTask } = await import('../../../src/commands/move.js')
50+
await expect(
51+
moveTask({ apiToken: 'pk_t', teamId: 'tm' }, 'task1', { to: 'list2', remove: 'list3' }),
52+
).rejects.toThrow(/Added task1 to list list2.*failed to remove.*list not found/)
53+
expect(mockAddTaskToList).toHaveBeenCalledWith('task1', 'list2')
54+
})
55+
56+
it('throws original error when remove fails without prior add', async () => {
57+
mockRemoveTaskFromList.mockRejectedValueOnce(new Error('list not found'))
58+
const { moveTask } = await import('../../../src/commands/move.js')
59+
await expect(
60+
moveTask({ apiToken: 'pk_t', teamId: 'tm' }, 'task1', { remove: 'list3' }),
61+
).rejects.toThrow('list not found')
62+
})
63+
4764
it('throws when neither --to nor --remove is provided', async () => {
4865
const { moveTask } = await import('../../../src/commands/move.js')
4966
await expect(moveTask({ apiToken: 'pk_t', teamId: 'tm' }, 'task1', {})).rejects.toThrow('--to')

tests/unit/commands/overdue.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ describe('fetchOverdueTasks', () => {
3535
mockGetMe.mockReset().mockResolvedValue({ id: 42, username: 'me' })
3636
})
3737

38+
it('passes includeClosed to getMyTasks when set', async () => {
39+
mockGetMyTasks.mockResolvedValue([])
40+
const { fetchOverdueTasks } = await import('../../../src/commands/overdue.js')
41+
await fetchOverdueTasks({ apiToken: 'pk_t', teamId: 'team1' }, { includeClosed: true })
42+
expect(mockGetMyTasks).toHaveBeenCalledWith('team1', { includeClosed: true })
43+
})
44+
45+
it('does not pass includeClosed when not set', async () => {
46+
mockGetMyTasks.mockResolvedValue([])
47+
const { fetchOverdueTasks } = await import('../../../src/commands/overdue.js')
48+
await fetchOverdueTasks({ apiToken: 'pk_t', teamId: 'team1' })
49+
expect(mockGetMyTasks).toHaveBeenCalledWith('team1', { includeClosed: undefined })
50+
})
51+
3852
it('returns tasks with due_date in the past', async () => {
3953
const pastDue = String(now - 24 * 60 * 60 * 1000)
4054
mockGetMyTasks.mockResolvedValue([makeTask('t1', 'to do', { due_date: pastDue })])

tests/unit/markdown.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,27 @@ describe('formatTaskDetailMarkdown', () => {
276276
expect(result.indexOf('## Description')).toBeGreaterThan(result.indexOf('**ID:**'))
277277
})
278278

279+
it('prefers markdown_content over description', () => {
280+
const task: Task = {
281+
...fullTask,
282+
description: 'plain text fallback',
283+
markdown_content: '# Rich **markdown** content',
284+
}
285+
const result = formatTaskDetailMarkdown(task)
286+
expect(result).toContain('# Rich **markdown** content')
287+
expect(result).not.toContain('plain text fallback')
288+
})
289+
290+
it('falls back to description when markdown_content is absent', () => {
291+
const task: Task = {
292+
...fullTask,
293+
description: 'plain description',
294+
markdown_content: undefined,
295+
}
296+
const result = formatTaskDetailMarkdown(task)
297+
expect(result).toContain('plain description')
298+
})
299+
279300
it('omits missing fields for a minimal task', () => {
280301
const minimal: Task = {
281302
id: 'min1',

0 commit comments

Comments
 (0)