* feat(s3): create bucket interactively during auth when unconfigured Problem: when a user runs `:Pending s3 auth` with no bucket configured, auth succeeds but offers no way to create the bucket. The user must manually run `aws s3api create-bucket` and update their config. Solution: add `util.input()` coroutine-aware prompt wrapper and a `create_bucket()` flow in `s3.lua` that prompts for bucket name and region, handles the `us-east-1` LocationConstraint quirk, and logs a config snippet on success. Called automatically from `auth()` when `sync.s3.bucket` is absent. * ci: typing * feat(parse): add `parse_duration_to_days` for duration string conversion Problem: The archive command accepted only a bare integer for days, inconsistent with the `+Nd`/`+Nw`/`+Nm` duration syntax used elsewhere. Solution: Add `parse_duration_to_days()` supporting `Nd`, `Nw`, `Nm`, and bare integers. Returns nil on invalid input for caller error handling. * feat(archive): duration syntax and confirmation prompt Problem: `:Pending archive` accepted only a bare integer for days and silently deleted tasks with no confirmation, risking accidental data loss. Solution: Accept duration strings (`7d`, `3w`, `2m`) via `parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation prompt before removing tasks, and skip the prompt when zero tasks match. * feat: add `<C-a>` / `<C-x>` keymaps for priority increment/decrement Problem: Priority could only be cycled with `g!` (0→1→2→3→0), with no way to directly increment or decrement. Solution: Add `adjust_priority()` with clamping at 0 and `max_priority`, exposed as `increment_priority()` / `decrement_priority()` on `<C-a>` / `<C-x>`. Includes `<Plug>` mappings and vimdoc. * fix(s3): use parenthetical defaults in bucket creation prompts Problem: `util.input` with `default` pre-filled the input field, and the success message said "Add to your config" ambiguously. Solution: Show defaults in prompt text as `(default)` instead of pre-filling, and clarify the message to "Add to your pending.nvim config". * ci: format * ci(sync): normalize log prefix to `backend:` across all sync backends Problem: Sync log messages used inconsistent prefixes like `s3 push:`, `gtasks pull:`, `gtasks sync —` instead of the `backend: action` pattern used by auth messages. Solution: Normalize all sync backend logs to `backend: action ...` format across `s3.lua`, `gcal.lua`, and `gtasks.lua`. * ci: fix linter warnings in archive spec and s3 bucket creation
297 lines
8.9 KiB
Lua
297 lines
8.9 KiB
Lua
require('spec.helpers')
|
|
|
|
local config = require('pending.config')
|
|
|
|
describe('archive', function()
|
|
local tmpdir
|
|
local pending
|
|
local ui_input_orig
|
|
|
|
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()
|
|
ui_input_orig = vim.ui.input
|
|
end)
|
|
|
|
after_each(function()
|
|
vim.ui.input = ui_input_orig
|
|
vim.fn.delete(tmpdir, 'rf')
|
|
vim.g.pending = nil
|
|
config.reset()
|
|
package.loaded['pending'] = nil
|
|
end)
|
|
|
|
local function auto_confirm_y()
|
|
vim.ui.input = function(_, on_confirm)
|
|
on_confirm('y')
|
|
end
|
|
end
|
|
|
|
local function auto_confirm_n()
|
|
vim.ui.input = function(_, on_confirm)
|
|
on_confirm('n')
|
|
end
|
|
end
|
|
|
|
it('removes done tasks completed more than 30 days ago', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old done task' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('keeps done tasks completed fewer than 30 days ago', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = s:add({ description = 'Recent done task' })
|
|
s:update(t.id, { status = 'done', ['end'] = recent_end })
|
|
pending.archive()
|
|
local active = s:active_tasks()
|
|
assert.are.equal(1, #active)
|
|
assert.are.equal('Recent done task', active[1].description)
|
|
end)
|
|
|
|
it('respects duration string 7d', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local eight_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (8 * 86400))
|
|
local t = s:add({ description = 'Old for 7 days' })
|
|
s:update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
|
pending.archive('7d')
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('respects duration string 2w', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local fifteen_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (15 * 86400))
|
|
local t = s:add({ description = 'Old for 2 weeks' })
|
|
s:update(t.id, { status = 'done', ['end'] = fifteen_days_ago })
|
|
pending.archive('2w')
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('respects duration string 2m', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old for 2 months' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive('2m')
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('respects bare integer as days (backwards compat)', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local eight_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (8 * 86400))
|
|
local t = s:add({ description = 'Old for 7 days' })
|
|
s:update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
|
pending.archive('7')
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('keeps tasks within the custom duration cutoff', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local five_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = s:add({ description = 'Recent for 7 days' })
|
|
s:update(t.id, { status = 'done', ['end'] = five_days_ago })
|
|
pending.archive('7d')
|
|
local active = s:active_tasks()
|
|
assert.are.equal(1, #active)
|
|
end)
|
|
|
|
it('errors on invalid duration input', function()
|
|
local messages = {}
|
|
local orig_notify = vim.notify
|
|
vim.notify = function(msg, ...)
|
|
table.insert(messages, msg)
|
|
return orig_notify(msg, ...)
|
|
end
|
|
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Task' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive('xyz')
|
|
|
|
vim.notify = orig_notify
|
|
assert.are.equal(1, #s:tasks())
|
|
|
|
local found = false
|
|
for _, msg in ipairs(messages) do
|
|
if msg:find('Invalid duration') then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
assert.is_true(found)
|
|
end)
|
|
|
|
it('never archives pending tasks regardless of age', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
s:add({ description = 'Still pending' })
|
|
pending.archive()
|
|
local active = s:active_tasks()
|
|
assert.are.equal(1, #active)
|
|
assert.are.equal('pending', active[1].status)
|
|
end)
|
|
|
|
it('removes deleted tasks past the cutoff', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old deleted task' })
|
|
s:update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
local all = s:tasks()
|
|
assert.are.equal(0, #all)
|
|
end)
|
|
|
|
it('keeps deleted tasks within the cutoff', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t = s:add({ description = 'Recent deleted' })
|
|
s:update(t.id, { status = 'deleted', ['end'] = recent_end })
|
|
pending.archive()
|
|
local all = s:tasks()
|
|
assert.are.equal(1, #all)
|
|
end)
|
|
|
|
it('skips confirmation and reports when no tasks match', function()
|
|
local input_called = false
|
|
vim.ui.input = function()
|
|
input_called = true
|
|
end
|
|
|
|
local messages = {}
|
|
local orig_notify = vim.notify
|
|
vim.notify = function(msg, ...)
|
|
table.insert(messages, msg)
|
|
return orig_notify(msg, ...)
|
|
end
|
|
|
|
local s = pending.store()
|
|
s:add({ description = 'Still pending' })
|
|
pending.archive()
|
|
|
|
vim.notify = orig_notify
|
|
assert.is_false(input_called)
|
|
|
|
local found = false
|
|
for _, msg in ipairs(messages) do
|
|
if msg:find('No tasks to archive') then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
assert.is_true(found)
|
|
end)
|
|
|
|
it('does not archive when user declines confirmation', function()
|
|
auto_confirm_n()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old done task' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
assert.are.equal(1, #s:tasks())
|
|
end)
|
|
|
|
it('does not archive when user cancels confirmation (nil)', function()
|
|
vim.ui.input = function(_, on_confirm)
|
|
on_confirm(nil)
|
|
end
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old done task' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
assert.are.equal(1, #s:tasks())
|
|
end)
|
|
|
|
it('reports the correct count in vim.notify', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local messages = {}
|
|
local orig_notify = vim.notify
|
|
vim.notify = function(msg, ...)
|
|
table.insert(messages, msg)
|
|
return orig_notify(msg, ...)
|
|
end
|
|
|
|
local t1 = s:add({ description = 'Old 1' })
|
|
local t2 = s:add({ description = 'Old 2' })
|
|
s:add({ description = 'Keep' })
|
|
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
s: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 after archive', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local t1 = s:add({ description = 'Old done' })
|
|
s:add({ description = 'Keep pending' })
|
|
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
|
local t3 = s:add({ description = 'Keep recent done' })
|
|
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
s:update(t3.id, { status = 'done', ['end'] = recent_end })
|
|
|
|
pending.archive()
|
|
|
|
local active = s: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)
|
|
|
|
it('persists archived tasks to disk after unload/reload', function()
|
|
auto_confirm_y()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Archived task' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive()
|
|
s:load()
|
|
assert.are.equal(0, #s:active_tasks())
|
|
end)
|
|
|
|
it('includes the duration in the confirmation prompt', function()
|
|
local prompt_text
|
|
vim.ui.input = function(opts, on_confirm)
|
|
prompt_text = opts.prompt
|
|
on_confirm('n')
|
|
end
|
|
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Old' })
|
|
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
|
pending.archive('3w')
|
|
|
|
assert.is_not_nil(prompt_text)
|
|
assert.truthy(prompt_text:find('21d'))
|
|
assert.truthy(prompt_text:find('1 task'))
|
|
end)
|
|
end)
|