local t = require('tests.harness') local Lock = require('arti-pack.lock') local spec = require('arti-pack.spec') local function tmpdir() local dir = vim.fn.tempname() vim.fn.mkdir(dir, 'p') return dir end local function write(path, contents) local fd = assert(io.open(path, 'w')) fd:write(contents) fd:close() end t.describe('lock', function() t.it('round-trips entries through the state file', function() local path = tmpdir() .. '/nested/lock.json' local lock = Lock.new(path) lock:set('plenary.nvim', { tag = 'v0.1.4', opt = false }) t.truthy(lock:save()) local reloaded = Lock.new(path):load() t.eq({ tag = 'v0.1.4', opt = false }, reloaded:get('plenary.nvim')) end) t.it('round-trips a branch entry with what identifies the fetched content', function() local path = tmpdir() .. '/lock.json' local entry = { branch = 'main', digest = string.rep('a', 64), installed_at = 1757000000, opt = false, source = 'hrsh7th/cmp-path', ext = 'zip', } local lock = Lock.new(path) lock:set('cmp-path', entry) t.truthy(lock:save()) t.eq(entry, Lock.new(path):load():get('cmp-path')) end) t.it('starts empty when the state file does not exist', function() local lock = Lock.new(tmpdir() .. '/absent.json'):load() t.eq({}, lock:names()) end) t.it('starts empty when the state file is corrupt', function() local path = tmpdir() .. '/lock.json' write(path, 'not json at all') t.eq({}, Lock.new(path):load():names()) end) t.it('starts empty when the state file has no plugins table', function() local path = tmpdir() .. '/lock.json' write(path, '{"version":1}') t.eq({}, Lock.new(path):load():names()) end) t.it('leaves no temporary file beside the state file', function() local dir = tmpdir() local lock = Lock.new(dir .. '/lock.json') lock:set('plenary.nvim', { tag = 'v0.1.4' }) t.truthy(lock:save()) local names = {} for _, entry in ipairs(vim.fn.readdir(dir)) do table.insert(names, entry) end t.eq({ 'lock.json' }, names) end) t.it('lists names in sorted order', function() local lock = Lock.new(tmpdir() .. '/lock.json') lock:set('zebra', { tag = 'v1' }) lock:set('alpha', { tag = 'v1' }) t.eq({ 'alpha', 'zebra' }, lock:names()) end) t.it('drops removed entries from a saved file', function() local path = tmpdir() .. '/lock.json' local lock = Lock.new(path) lock:set('gone', { tag = 'v1' }) lock:set('kept', { tag = 'v1' }) lock:remove('gone') lock:save() t.eq({ 'kept' }, Lock.new(path):load():names()) end) end) t.describe('lock.is_current', function() local function make(entry) local lock = Lock.new(tmpdir() .. '/lock.json') if entry then lock:set('repo', entry) end return lock end t.it('is false for a plugin that was never installed', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1' })) t.falsy(make(nil):is_current(s)) end) t.it('is true when the tag and load style both match', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1' })) t.truthy(make({ tag = 'v1', opt = false }):is_current(s)) end) t.it('is false when the tag changed', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v2' })) t.falsy(make({ tag = 'v1', opt = false }):is_current(s)) end) t.it('is false when the plugin moved between start and opt', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1', opt = true })) t.falsy(make({ tag = 'v1', opt = false }):is_current(s)) end) t.it('is true when the tracked branch still matches', function() local s = assert(spec.parse({ 'owner/repo', branch = 'main' })) t.truthy(make({ branch = 'main', opt = false }):is_current(s)) end) t.it('is false when the spec switched to another branch', function() local s = assert(spec.parse({ 'owner/repo', branch = 'master' })) t.falsy(make({ branch = 'main', opt = false }):is_current(s)) end) t.it('is false when a spec swaps between a tag and a branch', function() local tagged = assert(spec.parse({ 'owner/repo', tag = 'v1' })) local branched = assert(spec.parse({ 'owner/repo', branch = 'main' })) t.falsy(make({ branch = 'main', opt = false }):is_current(tagged)) t.falsy(make({ tag = 'v1', opt = false }):is_current(branched)) end) end) t.describe('lock.needs_install', function() local function make(entry) local lock = Lock.new(tmpdir() .. '/lock.json') if entry then lock:set('repo', entry) end return lock end local tagged = assert(spec.parse({ 'owner/repo', tag = 'v1' })) local branched = assert(spec.parse({ 'owner/repo', branch = 'main' })) t.it('installs anything that is missing from the lockfile', function() t.truthy(make(nil):needs_install(tagged, { present = false })) t.truthy(make(nil):needs_install(branched, { present = false })) end) t.it('installs anything whose directory is gone', function() t.truthy(make({ tag = 'v1' }):needs_install(tagged, { present = false })) t.truthy(make({ branch = 'main' }):needs_install(branched, { present = false })) end) t.it('skips an installed tag, which cannot have moved', function() t.falsy(make({ tag = 'v1' }):needs_install(tagged, { present = true })) t.falsy(make({ tag = 'v1' }):needs_install(tagged, { present = true, update = true })) end) t.it('leaves an installed branch alone on a plain sync', function() t.falsy(make({ branch = 'main' }):needs_install(branched, { present = true })) end) t.it('refetches an installed branch on an update run', function() t.truthy(make({ branch = 'main' }):needs_install(branched, { present = true, update = true })) end) t.it('refetches everything when forced', function() t.truthy(make({ tag = 'v1' }):needs_install(tagged, { present = true, force = true })) t.truthy(make({ branch = 'main' }):needs_install(branched, { present = true, force = true })) end) t.it('installs a branch whose name changed even without an update run', function() local other = assert(spec.parse({ 'owner/repo', branch = 'master' })) t.truthy(make({ branch = 'main' }):needs_install(other, { present = true })) end) end)