require('spec.helpers') local config = require('pending.config') describe('edit', function() local tmpdir local pending before_each(function() tmpdir = vim.fn.tempname() vim.fn.mkdir(tmpdir, 'p') vim.g.pending = { data_path = tmpdir .. '/tasks.json' } config.reset() package.loaded['pending'] = nil pending = require('pending') pending.store():load() end) after_each(function() vim.fn.delete(tmpdir, 'rf') vim.g.pending = nil config.reset() package.loaded['pending'] = nil end) it('sets due date with resolve_date vocabulary', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'due:tomorrow') local updated = s:get(t.id) local today = os.date('*t') --[[@as osdate]] local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) assert.are.equal(expected, updated.due) end) it('sets due date with literal YYYY-MM-DD', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'due:2026-06-15') local updated = s:get(t.id) assert.are.equal('2026-06-15', updated.due) end) it('sets category', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'cat:Work') local updated = s:get(t.id) assert.are.equal('Work', updated.category) end) it('adds priority', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), '+!') local updated = s:get(t.id) assert.are.equal(1, updated.priority) end) it('removes priority', function() local s = pending.store() local t = s:add({ description = 'Task one', priority = 1 }) s:save() pending.edit(tostring(t.id), '-!') local updated = s:get(t.id) assert.are.equal(0, updated.priority) end) it('removes due date', function() local s = pending.store() local t = s:add({ description = 'Task one', due = '2026-06-15' }) s:save() pending.edit(tostring(t.id), '-due') local updated = s:get(t.id) assert.is_nil(updated.due) end) it('removes category', function() local s = pending.store() local t = s:add({ description = 'Task one', category = 'Work' }) s:save() pending.edit(tostring(t.id), '-cat') local updated = s:get(t.id) assert.is_nil(updated.category) end) it('sets recurrence', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'rec:weekly') local updated = s:get(t.id) assert.are.equal('weekly', updated.recur) assert.is_nil(updated.recur_mode) end) it('sets completion-based recurrence', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'rec:!daily') local updated = s:get(t.id) assert.are.equal('daily', updated.recur) assert.are.equal('completion', updated.recur_mode) end) it('removes recurrence', function() local s = pending.store() local t = s:add({ description = 'Task one', recur = 'weekly', recur_mode = 'scheduled' }) s:save() pending.edit(tostring(t.id), '-rec') local updated = s:get(t.id) assert.is_nil(updated.recur) assert.is_nil(updated.recur_mode) end) it('applies multiple operations at once', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'due:today cat:Errands +!') local updated = s:get(t.id) assert.are.equal(os.date('%Y-%m-%d'), updated.due) assert.are.equal('Errands', updated.category) assert.are.equal(1, updated.priority) end) it('pushes to undo stack', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local stack_before = #s:undo_stack() pending.edit(tostring(t.id), 'cat:Work') assert.are.equal(stack_before + 1, #s:undo_stack()) end) it('persists changes to disk', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'cat:Work') s:load() local updated = s:get(t.id) assert.are.equal('Work', updated.category) end) it('errors on unknown task ID', function() local s = pending.store() s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit('999', 'cat:Work') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('No task with ID 999')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors on invalid date', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit(tostring(t.id), 'due:notadate') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Invalid date')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors on unknown operation token', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit(tostring(t.id), 'bogus') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Unknown operation')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors on invalid recurrence pattern', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit(tostring(t.id), 'rec:nope') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Invalid recurrence')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors when no operations given', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit(tostring(t.id), '') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Usage')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors when no id given', function() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit('', '') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Usage')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('errors on non-numeric id', function() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit('abc', 'cat:Work') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Invalid task ID')) assert.are.equal(vim.log.levels.ERROR, messages[1].level) end) it('shows feedback message on success', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() local messages = {} local orig_notify = vim.notify vim.notify = function(msg, level) table.insert(messages, { msg = msg, level = level }) end pending.edit(tostring(t.id), 'cat:Work') vim.notify = orig_notify assert.are.equal(1, #messages) assert.truthy(messages[1].msg:find('Task #' .. t.id .. ' updated')) assert.truthy(messages[1].msg:find('category set to Work')) end) it('respects custom date_syntax', function() vim.g.pending = { data_path = tmpdir .. '/tasks.json', date_syntax = 'by' } config.reset() package.loaded['pending'] = nil pending = require('pending') local s = pending.store() s:load() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'by:tomorrow') local updated = s:get(t.id) local today = os.date('*t') --[[@as osdate]] local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) assert.are.equal(expected, updated.due) end) it('respects custom recur_syntax', function() vim.g.pending = { data_path = tmpdir .. '/tasks.json', recur_syntax = 'repeat' } config.reset() package.loaded['pending'] = nil pending = require('pending') local s = pending.store() s:load() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'repeat:weekly') local updated = s:get(t.id) assert.are.equal('weekly', updated.recur) end) it('does not modify store on error', function() local s = pending.store() local t = s:add({ description = 'Task one', category = 'Original' }) s:save() local orig_notify = vim.notify vim.notify = function() end pending.edit(tostring(t.id), 'due:notadate') vim.notify = orig_notify local updated = s:get(t.id) assert.are.equal('Original', updated.category) assert.is_nil(updated.due) end) it('sets due date with datetime format', function() local s = pending.store() local t = s:add({ description = 'Task one' }) s:save() pending.edit(tostring(t.id), 'due:tomorrow@14:00') local updated = s:get(t.id) local today = os.date('*t') --[[@as osdate]] local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) assert.are.equal(expected .. 'T14:00', updated.due) end) end)