refactor: organize tests and dry (#49)
* 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
This commit is contained in:
parent
dbd76d6759
commit
41bda24570
19 changed files with 819 additions and 703 deletions
|
|
@ -2,7 +2,6 @@ require('spec.helpers')
|
|||
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('filter', function()
|
||||
local tmpdir
|
||||
|
|
@ -14,32 +13,31 @@ describe('filter', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
pending = require('pending')
|
||||
buffer = require('pending.buffer')
|
||||
buffer.set_filter({}, {})
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
end)
|
||||
|
||||
describe('filter predicates', function()
|
||||
it('cat: hides tasks with non-matching category', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_task = nil
|
||||
local home_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
|
|
@ -57,13 +55,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('cat: hides tasks with no category (default category)', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Inbox task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Inbox task' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local inbox_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category ~= 'Work' then
|
||||
|
|
@ -75,14 +73,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('overdue hides non-overdue tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.add({ description = 'No due task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:add({ description = 'No due task' })
|
||||
s:save()
|
||||
pending.filter('overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local overdue_task, future_task, nodue_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == '2020-01-01' then
|
||||
|
|
@ -101,15 +99,15 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('today hides non-today tasks', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.save()
|
||||
s:add({ description = 'Today task', due = today })
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:save()
|
||||
pending.filter('today')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local today_task, old_task, future_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == today then
|
||||
|
|
@ -128,13 +126,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('priority hides non-priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Important', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Important', priority = 1 })
|
||||
s:add({ description = 'Normal' })
|
||||
s:save()
|
||||
pending.filter('priority')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local important_task, normal_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.priority and t.priority > 0 then
|
||||
|
|
@ -149,14 +147,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('multi-predicate AND: cat:Work + overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
store.add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
store.add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
s:add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
s:add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending.filter('cat:Work overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_overdue, work_future, home_overdue
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Work overdue' then
|
||||
|
|
@ -175,10 +173,10 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter clear removes all predicates and hidden ids', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('clear')
|
||||
|
|
@ -187,9 +185,9 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter empty string clears filter', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('')
|
||||
|
|
@ -197,16 +195,16 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter predicates persist across set_filter calls', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local preds = buffer.filter_predicates()
|
||||
assert.are.equal(1, #preds)
|
||||
assert.are.equal('cat:Work', preds[1])
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local home_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category == 'Home' then
|
||||
|
|
@ -219,11 +217,11 @@ describe('filter', function()
|
|||
|
||||
describe('diff.apply with hidden_ids', function()
|
||||
it('does not mark hidden tasks as deleted', function()
|
||||
store.load()
|
||||
store.add({ description = 'Visible task' })
|
||||
store.add({ description = 'Hidden task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Visible task' })
|
||||
s:add({ description = 'Hidden task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local hidden_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Hidden task' then
|
||||
|
|
@ -234,19 +232,18 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/1/- [ ] Visible task',
|
||||
}
|
||||
diff.apply(lines, hidden_ids)
|
||||
store.unload()
|
||||
store.load()
|
||||
local hidden = store.get(hidden_task.id)
|
||||
diff.apply(lines, s, hidden_ids)
|
||||
s:load()
|
||||
local hidden = s:get(hidden_task.id)
|
||||
assert.are.equal('pending', hidden.status)
|
||||
end)
|
||||
|
||||
it('marks tasks deleted when not hidden and not in buffer', function()
|
||||
store.load()
|
||||
store.add({ description = 'Keep task' })
|
||||
store.add({ description = 'Delete task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Keep task' })
|
||||
s:add({ description = 'Delete task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local keep_task, delete_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Keep task' then
|
||||
|
|
@ -259,27 +256,25 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/' .. keep_task.id .. '/- [ ] Keep task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local deleted = store.get(delete_task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local deleted = s:get(delete_task.id)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
end)
|
||||
|
||||
it('strips FILTER: line before parsing', function()
|
||||
store.load()
|
||||
store.add({ description = 'My task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'My task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local task = tasks[1]
|
||||
local lines = {
|
||||
'FILTER: cat:Work',
|
||||
'/' .. task.id .. '/- [ ] My task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.get(task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local t = s:get(task.id)
|
||||
assert.are.equal('pending', t.status)
|
||||
end)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue