* refactor(store): convert singleton to Store.new() factory
Problem: store.lua used module-level _data singleton, making
project-local stores impossible and creating hidden global state.
Solution: introduce Store metatable with all operations as instance
methods. M.new(path) constructs an instance; M.resolve_path()
searches upward for .pending.json and falls back to
config.get().data_path. Singleton module API is removed.
* refactor(diff): accept store instance as parameter
Problem: diff.apply called store singleton methods directly, coupling
it to global state and preventing use with project-local stores.
Solution: change signature to apply(lines, s, hidden_ids?) where s is
a pending.Store instance. All store operations now go through s.
* refactor(buffer): add set_store/store accessors, drop singleton dep
Problem: buffer.lua imported store directly and called singleton
methods, preventing it from working with per-project store instances.
Solution: add module-level _store, M.set_store(s), and M.store()
accessors. open() and render() use _store instead of the singleton.
init.lua will call buffer.set_store(s) before buffer.open().
* refactor(complete,health,sync,plugin): update callers to store instance API
Problem: complete.lua, health.lua, sync/gcal.lua, and plugin/pending.lua
all called singleton store methods directly.
Solution: complete.lua uses buffer.store() for category lookups;
health.lua uses store.new(store.resolve_path()) and reports the
resolved path; gcal.lua calls require('pending').store() for task
access; plugin tab-completion creates ephemeral store instances via
store.new(store.resolve_path()). Add 'init' to the subcommands list.
* feat(init): thread Store instance through init, add :Pending init
Problem: init.lua called singleton store methods throughout, and there
was no way to create a project-local .pending.json file.
Solution: add module-level _store and private get_store() that
lazy-constructs via store.new(store.resolve_path()). Add public
M.store() accessor used by specs and sync backends. M.open() calls
buffer.set_store(get_store()) before buffer.open(). All store
callsites converted to get_store():method(). goto_file() and
add_here() derive the data directory from get_store().path.
Add M.init() which creates .pending.json in cwd and dispatches from
M.command() as ':Pending init'.
* test: update all specs for Store instance API
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* docs(pending): document :Pending init and store resolution
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
* ci: format
* ci: remove unused variable
260 lines
7.6 KiB
Lua
260 lines
7.6 KiB
Lua
require('spec.helpers')
|
|
|
|
local config = require('pending.config')
|
|
local parse = require('pending.parse')
|
|
|
|
describe('status', 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)
|
|
|
|
describe('counts', function()
|
|
it('returns zeroes for empty store', function()
|
|
local c = pending.counts()
|
|
assert.are.equal(0, c.overdue)
|
|
assert.are.equal(0, c.today)
|
|
assert.are.equal(0, c.pending)
|
|
assert.are.equal(0, c.priority)
|
|
assert.is_nil(c.next_due)
|
|
end)
|
|
|
|
it('counts pending tasks', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'One' })
|
|
s:add({ description = 'Two' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(2, c.pending)
|
|
end)
|
|
|
|
it('counts priority tasks', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Urgent', priority = 1 })
|
|
s:add({ description = 'Normal' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.priority)
|
|
end)
|
|
|
|
it('counts overdue tasks with date-only', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Old task', due = '2020-01-01' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.overdue)
|
|
end)
|
|
|
|
it('counts overdue tasks with datetime', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Old task', due = '2020-01-01T08:00' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.overdue)
|
|
end)
|
|
|
|
it('counts today tasks', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Today task', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.today)
|
|
assert.are.equal(0, c.overdue)
|
|
end)
|
|
|
|
it('counts mixed overdue and today', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Overdue', due = '2020-01-01' })
|
|
s:add({ description = 'Today', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.overdue)
|
|
assert.are.equal(1, c.today)
|
|
end)
|
|
|
|
it('excludes done tasks', function()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Done', due = '2020-01-01' })
|
|
s:update(t.id, { status = 'done' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(0, c.overdue)
|
|
assert.are.equal(0, c.pending)
|
|
end)
|
|
|
|
it('excludes deleted tasks', function()
|
|
local s = pending.store()
|
|
local t = s:add({ description = 'Deleted', due = '2020-01-01' })
|
|
s:delete(t.id)
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(0, c.overdue)
|
|
assert.are.equal(0, c.pending)
|
|
end)
|
|
|
|
it('excludes someday sentinel', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Someday', due = '9999-12-30' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(0, c.overdue)
|
|
assert.are.equal(0, c.today)
|
|
assert.are.equal(1, c.pending)
|
|
end)
|
|
|
|
it('picks earliest future date as next_due', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Soon', due = '2099-06-01' })
|
|
s:add({ description = 'Sooner', due = '2099-03-01' })
|
|
s:add({ description = 'Today', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
local c = pending.counts()
|
|
assert.are.equal(today, c.next_due)
|
|
end)
|
|
|
|
it('lazy loads on first counts() call', function()
|
|
local path = config.get().data_path
|
|
local f = io.open(path, 'w')
|
|
f:write(vim.json.encode({
|
|
version = 1,
|
|
next_id = 2,
|
|
tasks = {
|
|
{
|
|
id = 1,
|
|
description = 'Overdue',
|
|
status = 'pending',
|
|
due = '2020-01-01',
|
|
entry = '2020-01-01T00:00:00Z',
|
|
modified = '2020-01-01T00:00:00Z',
|
|
},
|
|
},
|
|
}))
|
|
f:close()
|
|
package.loaded['pending'] = nil
|
|
pending = require('pending')
|
|
local c = pending.counts()
|
|
assert.are.equal(1, c.overdue)
|
|
end)
|
|
end)
|
|
|
|
describe('statusline', function()
|
|
it('returns empty string when nothing actionable', function()
|
|
local s = pending.store()
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.are.equal('', pending.statusline())
|
|
end)
|
|
|
|
it('formats overdue only', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Old', due = '2020-01-01' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.are.equal('1 overdue', pending.statusline())
|
|
end)
|
|
|
|
it('formats today only', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Today', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.are.equal('1 today', pending.statusline())
|
|
end)
|
|
|
|
it('formats overdue and today', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Old', due = '2020-01-01' })
|
|
s:add({ description = 'Today', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.are.equal('1 overdue, 1 today', pending.statusline())
|
|
end)
|
|
end)
|
|
|
|
describe('has_due', function()
|
|
it('returns false when nothing due', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Future', due = '2099-01-01' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.is_false(pending.has_due())
|
|
end)
|
|
|
|
it('returns true when overdue', function()
|
|
local s = pending.store()
|
|
s:add({ description = 'Old', due = '2020-01-01' })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.is_true(pending.has_due())
|
|
end)
|
|
|
|
it('returns true when today', function()
|
|
local s = pending.store()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
s:add({ description = 'Now', due = today })
|
|
s:save()
|
|
pending._recompute_counts()
|
|
assert.is_true(pending.has_due())
|
|
end)
|
|
end)
|
|
|
|
describe('parse.is_overdue', function()
|
|
it('date before today is overdue', function()
|
|
assert.is_true(parse.is_overdue('2020-01-01'))
|
|
end)
|
|
|
|
it('date after today is not overdue', function()
|
|
assert.is_false(parse.is_overdue('2099-01-01'))
|
|
end)
|
|
|
|
it('today date-only is not overdue', function()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
assert.is_false(parse.is_overdue(today))
|
|
end)
|
|
end)
|
|
|
|
describe('parse.is_today', function()
|
|
it('today date-only is today', function()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
assert.is_true(parse.is_today(today))
|
|
end)
|
|
|
|
it('yesterday is not today', function()
|
|
assert.is_false(parse.is_today('2020-01-01'))
|
|
end)
|
|
|
|
it('tomorrow is not today', function()
|
|
assert.is_false(parse.is_today('2099-01-01'))
|
|
end)
|
|
end)
|
|
end)
|