require('spec.helpers') local config = require('pending.config') describe('detail frontmatter', function() local buffer local tmpdir 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 package.loaded['pending.buffer'] = nil buffer = require('pending.buffer') end) after_each(function() vim.fn.delete(tmpdir, 'rf') vim.g.pending = nil config.reset() package.loaded['pending'] = nil package.loaded['pending.buffer'] = nil end) describe('build_detail_frontmatter', function() it('renders status and priority for minimal task', function() local lines = buffer._build_detail_frontmatter({ id = 1, description = 'Test', status = 'pending', priority = 0, entry = '', modified = '', order = 0, }) assert.are.equal(2, #lines) assert.are.equal('Status: pending', lines[1]) assert.are.equal('Priority: 0', lines[2]) end) it('renders all fields', function() local lines = buffer._build_detail_frontmatter({ id = 1, description = 'Test', status = 'wip', priority = 2, category = 'Work', due = '2026-03-15', recur = 'weekly', entry = '', modified = '', order = 0, }) assert.are.equal(5, #lines) assert.are.equal('Status: wip', lines[1]) assert.are.equal('Priority: 2', lines[2]) assert.are.equal('Category: Work', lines[3]) assert.are.equal('Due: 2026-03-15', lines[4]) assert.are.equal('Recur: weekly', lines[5]) end) it('prefixes recur with ! for completion mode', function() local lines = buffer._build_detail_frontmatter({ id = 1, description = 'Test', status = 'pending', priority = 0, recur = 'daily', recur_mode = 'completion', entry = '', modified = '', order = 0, }) assert.are.equal('Recur: !daily', lines[3]) end) it('omits optional fields when absent', function() local lines = buffer._build_detail_frontmatter({ id = 1, description = 'Test', status = 'done', priority = 1, entry = '', modified = '', order = 0, }) assert.are.equal(2, #lines) assert.are.equal('Status: done', lines[1]) assert.are.equal('Priority: 1', lines[2]) end) end) describe('parse_detail_frontmatter', function() it('parses minimal frontmatter', function() local lines = { '# My task', 'Status: pending', 'Priority: 0', '---', 'some notes', } local sep, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal(4, sep) assert.are.equal('My task', fields.description) assert.are.equal('pending', fields.status) assert.are.equal(0, fields.priority) end) it('parses all fields', function() local lines = { '# Fix the bug', 'Status: wip', 'Priority: 2', 'Category: Work', 'Due: 2026-03-15', 'Recur: weekly', '---', } local sep, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal(7, sep) assert.are.equal('Fix the bug', fields.description) assert.are.equal('wip', fields.status) assert.are.equal(2, fields.priority) assert.are.equal('Work', fields.category) assert.are.equal('2026-03-15', fields.due) assert.are.equal('weekly', fields.recur) end) it('resolves due date keywords', function() local lines = { '# Task', 'Status: pending', 'Priority: 0', 'Due: tomorrow', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) local today = os.date('*t') --[[@as osdate]] local expected = os.date( '%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }) ) assert.are.equal(expected, fields.due) end) it('parses completion-mode recurrence', function() local lines = { '# Task', 'Status: pending', 'Priority: 0', 'Recur: !daily', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal('daily', fields.recur) assert.are.equal('completion', fields.recur_mode) end) it('clears optional fields when lines removed', function() local lines = { '# Task', 'Status: done', 'Priority: 1', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal(vim.NIL, fields.category) assert.are.equal(vim.NIL, fields.due) assert.are.equal(vim.NIL, fields.recur) end) it('skips blank lines in frontmatter', function() local lines = { '# Task', 'Status: pending', '', 'Priority: 0', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal('pending', fields.status) assert.are.equal(0, fields.priority) end) it('errors on missing separator', function() local lines = { '# Task', 'Status: pending', 'Priority: 0', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('missing separator')) end) it('errors on missing title', function() local lines = { '', 'Status: pending', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('missing or empty title')) end) it('errors on empty title', function() local lines = { '# ', 'Status: pending', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('missing or empty title')) end) it('errors on invalid status', function() local lines = { '# Task', 'Status: bogus', 'Priority: 0', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid status')) end) it('errors on negative priority', function() local lines = { '# Task', 'Status: pending', 'Priority: -1', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid priority')) end) it('errors on non-integer priority', function() local lines = { '# Task', 'Status: pending', 'Priority: 1.5', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid priority')) end) it('errors on priority exceeding max', function() local lines = { '# Task', 'Status: pending', 'Priority: 4', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('max is 3')) end) it('errors on invalid due date', function() local lines = { '# Task', 'Status: pending', 'Priority: 0', 'Due: notadate', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid due date')) end) it('errors on empty due value', function() local lines = { '# Task', 'Due: ', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('empty due value')) end) it('errors on invalid recurrence', function() local lines = { '# Task', 'Recur: nope', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid recurrence')) end) it('errors on empty recur value', function() local lines = { '# Task', 'Recur: ', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('empty recur value')) end) it('errors on empty category value', function() local lines = { '# Task', 'Category: ', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('empty category')) end) it('errors on unknown field', function() local lines = { '# Task', 'Status: pending', 'Foo: bar', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('unknown field: foo')) end) it('errors on duplicate field', function() local lines = { '# Task', 'Status: pending', 'Status: done', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('duplicate field')) end) it('errors on malformed frontmatter line', function() local lines = { '# Task', 'not a key value pair', '---', } local _, _, err = buffer._parse_detail_frontmatter(lines) assert.truthy(err:find('invalid frontmatter line')) end) it('is case-insensitive for field keys', function() local lines = { '# Task', 'STATUS: wip', 'PRIORITY: 1', 'CATEGORY: Work', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal('wip', fields.status) assert.are.equal(1, fields.priority) assert.are.equal('Work', fields.category) end) it('accepts datetime due format', function() local lines = { '# Task', 'Due: 2026-03-15T14:00', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal('2026-03-15T14:00', fields.due) end) it('respects custom max_priority', function() vim.g.pending = { data_path = tmpdir .. '/tasks.json', max_priority = 5 } config.reset() local lines = { '# Task', 'Priority: 5', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal(5, fields.priority) end) it('updates description from title line', function() local lines = { '# Updated title', 'Status: pending', 'Priority: 0', '---', } local _, fields, err = buffer._parse_detail_frontmatter(lines) assert.is_nil(err) assert.are.equal('Updated title', fields.description) end) end) end)