* feat(config): add category_order field Problem: category display order was always insertion order with no way to configure it. Solution: add category_order to config defaults so users can declare a preferred category ordering; unspecified categories append after. * feat(parse): add relative date resolution Problem: due dates required full YYYY-MM-DD input, adding friction for common cases like "today" or "next monday". Solution: add resolve_date() supporting today, tomorrow, +Nd, and weekday abbreviations; extend inline token parsing to resolve relative values before falling back to strict date validation. * feat(views): overdue flag, category in priority view, category ordering Problem: overdue tasks were visually indistinct from upcoming ones; priority view had no category context; category display order was not configurable. Solution: compute overdue meta flag for pending tasks past their due date; set show_category on priority view task meta; reorder categories according to config.category_order when present. * feat(buffer): overdue highlight, category virt text in priority view Problem: overdue tasks had no visual distinction; priority view showed no category context alongside due dates. Solution: add PendingOverdue highlight group; render category name as right-aligned virtual text in priority view, composited with the due date when both are present. * feat(init): undo write and buffer-local default mappings Problem: _undo_state was captured on every save but never consumed; toggle_priority and prompt_date had no buffer-local defaults, requiring manual <Plug> configuration. Solution: implement undo_write() to restore pre-save task state; add !, d, and U as buffer-local defaults following fugitive's philosophy of owning the buffer; expose :Pending undo as a command alias. * test(views): add views spec Problem: views.lua had no test coverage. Solution: add 26 tests covering category_view and priority_view including sort order, line format, overdue detection, show_category meta, and category_order config behavior. * test(archive): add archive spec Problem: archive had no test coverage. Solution: add 9 tests covering cutoff logic, custom day counts, pending task preservation, deleted task cleanup, and notify output. * docs: add vimdoc Problem: no :help documentation existed. Solution: add doc/pending.txt covering all features — commands, mappings, views, configuration, Google Calendar sync, highlight groups, data format, and health check — following standard vimdoc conventions. * ci: format * fix: resolve lint and type check errors Problem: selene flagged unused variables in new spec files; LuaLS flagged os.date/os.time return type mismatches, integer? assignments, and stale task.Task/task.GcalConfig type references. Solution: prefix unused spec variables with _ or drop unnecessary assignments; add --[[@as string/integer]] casts for os.date and os.time calls; add category_order field to pending.Config annotation; fix task.GcalConfig -> pending.GcalConfig and task.Task[] -> pending.Task[]; add nil guards on meta[row].id before store calls; cast store.data() return to non-optional. * ci: format * fix: sync * ci: format
131 lines
4.3 KiB
Lua
131 lines
4.3 KiB
Lua
require('spec.helpers')
|
|
|
|
local config = require('pending.config')
|
|
local store = require('pending.store')
|
|
|
|
describe('archive', function()
|
|
local tmpdir
|
|
local pending = require('pending')
|
|
|
|
before_each(function()
|
|
tmpdir = vim.fn.tempname()
|
|
vim.fn.mkdir(tmpdir, 'p')
|
|
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
|
config.reset()
|
|
store.unload()
|
|
store.load()
|
|
end)
|
|
|
|
after_each(function()
|
|
vim.fn.delete(tmpdir, 'rf')
|
|
vim.g.pending = nil
|
|
config.reset()
|
|
end)
|
|
|
|
it('removes done tasks completed more than 30 days ago', function()
|
|
local t = store.add({ description = 'Old done task' })
|
|
store.update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
assert.are.equal(0, #store.active_tasks())
|
|
end)
|
|
|
|
it('keeps done tasks completed fewer than 30 days ago', function()
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = store.add({ description = 'Recent done task' })
|
|
store.update(t.id, { status = 'done', ['end'] = recent_end })
|
|
pending.archive()
|
|
local active = store.active_tasks()
|
|
assert.are.equal(1, #active)
|
|
assert.are.equal('Recent done task', active[1].description)
|
|
end)
|
|
|
|
it('respects a custom day count', function()
|
|
local eight_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (8 * 86400))
|
|
local t = store.add({ description = 'Old for 7 days' })
|
|
store.update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
|
pending.archive(7)
|
|
assert.are.equal(0, #store.active_tasks())
|
|
end)
|
|
|
|
it('keeps tasks within the custom day cutoff', function()
|
|
local five_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = store.add({ description = 'Recent for 7 days' })
|
|
store.update(t.id, { status = 'done', ['end'] = five_days_ago })
|
|
pending.archive(7)
|
|
local active = store.active_tasks()
|
|
assert.are.equal(1, #active)
|
|
end)
|
|
|
|
it('never archives pending tasks regardless of age', function()
|
|
store.add({ description = 'Still pending' })
|
|
pending.archive()
|
|
local active = store.active_tasks()
|
|
assert.are.equal(1, #active)
|
|
assert.are.equal('pending', active[1].status)
|
|
end)
|
|
|
|
it('removes deleted tasks past the cutoff', function()
|
|
local t = store.add({ description = 'Old deleted task' })
|
|
store.update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
local all = store.tasks()
|
|
assert.are.equal(0, #all)
|
|
end)
|
|
|
|
it('keeps deleted tasks within the cutoff', function()
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = store.add({ description = 'Recent deleted' })
|
|
store.update(t.id, { status = 'deleted', ['end'] = recent_end })
|
|
pending.archive()
|
|
local all = store.tasks()
|
|
assert.are.equal(1, #all)
|
|
end)
|
|
|
|
it('reports the correct count in vim.notify', function()
|
|
local messages = {}
|
|
local orig_notify = vim.notify
|
|
vim.notify = function(msg, ...)
|
|
table.insert(messages, msg)
|
|
return orig_notify(msg, ...)
|
|
end
|
|
|
|
local t1 = store.add({ description = 'Old 1' })
|
|
local t2 = store.add({ description = 'Old 2' })
|
|
store.add({ description = 'Keep' })
|
|
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
store.update(t2.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
|
|
pending.archive()
|
|
|
|
vim.notify = orig_notify
|
|
|
|
local found = false
|
|
for _, msg in ipairs(messages) do
|
|
if msg:find('Archived 2') then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
assert.is_true(found)
|
|
end)
|
|
|
|
it('leaves only kept tasks in store.active_tasks after archive', function()
|
|
local t1 = store.add({ description = 'Old done' })
|
|
store.add({ description = 'Keep pending' })
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t3 = store.add({ description = 'Keep recent done' })
|
|
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
store.update(t3.id, { status = 'done', ['end'] = recent_end })
|
|
|
|
pending.archive()
|
|
|
|
local active = store.active_tasks()
|
|
assert.are.equal(2, #active)
|
|
local descs = {}
|
|
for _, task in ipairs(active) do
|
|
descs[task.description] = true
|
|
end
|
|
assert.is_true(descs['Keep pending'])
|
|
assert.is_true(descs['Keep recent done'])
|
|
end)
|
|
end)
|