require('spec.helpers') local config = require('pending.config') local store = require('pending.store') describe('store', function() local tmpdir local s before_each(function() tmpdir = vim.fn.tempname() vim.fn.mkdir(tmpdir, 'p') s = store.new(tmpdir .. '/tasks.json') s:load() end) after_each(function() vim.fn.delete(tmpdir, 'rf') config.reset() end) describe('load', function() it('returns empty data when no file exists', function() local data = s:load() assert.are.equal(1, data.version) assert.are.equal(1, data.next_id) assert.are.same({}, data.tasks) end) it('loads existing data', function() local path = tmpdir .. '/tasks.json' local f = io.open(path, 'w') f:write(vim.json.encode({ version = 1, next_id = 3, tasks = { { id = 1, description = 'Pending one', status = 'pending', entry = '2026-01-01T00:00:00Z', modified = '2026-01-01T00:00:00Z', }, { id = 2, description = 'Pending two', status = 'done', entry = '2026-01-01T00:00:00Z', modified = '2026-01-01T00:00:00Z', }, }, })) f:close() local data = s:load() assert.are.equal(3, data.next_id) assert.are.equal(2, #data.tasks) assert.are.equal('Pending one', data.tasks[1].description) assert.are.equal('done', data.tasks[2].status) end) it('preserves unknown fields', function() local path = tmpdir .. '/tasks.json' local f = io.open(path, 'w') f:write(vim.json.encode({ version = 1, next_id = 2, tasks = { { id = 1, description = 'Pending', status = 'pending', entry = '2026-01-01T00:00:00Z', modified = '2026-01-01T00:00:00Z', custom_field = 'hello', }, }, })) f:close() s:load() local task = s:get(1) assert.is_not_nil(task._extra) assert.are.equal('hello', task._extra.custom_field) end) end) describe('add', function() it('creates a task with incremented id', function() local t1 = s:add({ description = 'First' }) local t2 = s:add({ description = 'Second' }) assert.are.equal(1, t1.id) assert.are.equal(2, t2.id) assert.are.equal('pending', t1.status) assert.are.equal('Todo', t1.category) end) it('uses provided category', function() local t = s:add({ description = 'Test', category = 'Work' }) assert.are.equal('Work', t.category) end) end) describe('update', function() it('updates fields and sets modified', function() local t = s:add({ description = 'Original' }) t.modified = '2025-01-01T00:00:00Z' s:update(t.id, { description = 'Updated' }) local updated = s:get(t.id) assert.are.equal('Updated', updated.description) assert.is_not.equal('2025-01-01T00:00:00Z', updated.modified) end) it('sets end timestamp on completion', function() local t = s:add({ description = 'Test' }) assert.is_nil(t['end']) s:update(t.id, { status = 'done' }) local updated = s:get(t.id) assert.is_not_nil(updated['end']) end) it('does not overwrite id or entry', function() local t = s:add({ description = 'Immutable fields' }) local original_id = t.id local original_entry = t.entry s:update(t.id, { id = 999, entry = 'x' }) local updated = s:get(original_id) assert.are.equal(original_id, updated.id) assert.are.equal(original_entry, updated.entry) end) it('does not overwrite end on second completion', function() local t = s:add({ description = 'Complete twice' }) s:update(t.id, { status = 'done', ['end'] = '2026-01-15T10:00:00Z' }) local first_end = s:get(t.id)['end'] s:update(t.id, { status = 'done' }) local task = s:get(t.id) assert.are.equal(first_end, task['end']) end) end) describe('delete', function() it('marks task as deleted', function() local t = s:add({ description = 'To delete' }) s:delete(t.id) local deleted = s:get(t.id) assert.are.equal('deleted', deleted.status) assert.is_not_nil(deleted['end']) end) end) describe('save and round-trip', function() it('persists and reloads correctly', function() s:add({ description = 'Persisted', category = 'Work', priority = 1 }) s:save() s:load() local tasks = s:active_tasks() assert.are.equal(1, #tasks) assert.are.equal('Persisted', tasks[1].description) assert.are.equal('Work', tasks[1].category) assert.are.equal(1, tasks[1].priority) end) it('round-trips unknown fields', function() local path = tmpdir .. '/tasks.json' local f = io.open(path, 'w') f:write(vim.json.encode({ version = 1, next_id = 2, tasks = { { id = 1, description = 'Pending', status = 'pending', entry = '2026-01-01T00:00:00Z', modified = '2026-01-01T00:00:00Z', _gcal_event_id = 'abc123', }, }, })) f:close() s:load() s:save() s:load() local task = s:get(1) assert.are.equal('abc123', task._extra._gcal_event_id) end) end) describe('recurrence fields', function() it('persists recur and recur_mode through round-trip', function() s:add({ description = 'Recurring', recur = 'weekly', recur_mode = 'scheduled' }) s:save() s:load() local task = s:get(1) assert.are.equal('weekly', task.recur) assert.are.equal('scheduled', task.recur_mode) end) it('persists recur without recur_mode', function() s:add({ description = 'Simple recur', recur = 'daily' }) s:save() s:load() local task = s:get(1) assert.are.equal('daily', task.recur) assert.is_nil(task.recur_mode) end) it('omits recur fields when not set', function() s:add({ description = 'No recur' }) s:save() s:load() local task = s:get(1) assert.is_nil(task.recur) assert.is_nil(task.recur_mode) end) end) describe('active_tasks', function() it('excludes deleted tasks', function() s:add({ description = 'Active' }) local t2 = s:add({ description = 'To delete' }) s:delete(t2.id) local active = s:active_tasks() assert.are.equal(1, #active) assert.are.equal('Active', active[1].description) end) end) describe('snapshot', function() it('returns a table of tasks', function() s:add({ description = 'Snap one' }) s:add({ description = 'Snap two' }) local snap = s:snapshot() assert.are.equal(2, #snap) end) it('returns a copy that does not affect the store', function() local t = s:add({ description = 'Original' }) local snap = s:snapshot() snap[1].description = 'Mutated' local live = s:get(t.id) assert.are.equal('Original', live.description) end) it('excludes deleted tasks', function() local t = s:add({ description = 'Will be deleted' }) s:delete(t.id) local snap = s:snapshot() assert.are.equal(0, #snap) end) end) end)