local t = require('tests.harness') local spec = require('arti-pack.spec') local REGISTRY = 'https://artifacts.example.com/artifactory/github-remote' t.describe('spec.parse', function() t.it('parses a full table spec', function() local s = assert(spec.parse({ 'nvim-lua/plenary.nvim', tag = 'v0.1.4', opt = true })) t.eq('nvim-lua', s.owner) t.eq('plenary.nvim', s.repo) t.eq('plenary.nvim', s.name) t.eq('v0.1.4', s.tag) t.eq('zip', s.ext) t.eq(true, s.opt) end) t.it('defaults opt to false', function() local s = assert(spec.parse({ 'tpope/vim-fugitive', tag = 'v3.7' })) t.eq(false, s.opt) end) t.it('honours an "as" override for the install directory', function() local s = assert(spec.parse({ 'folke/tokyonight.nvim', tag = 'v4.10.0', as = 'tokyonight' })) t.eq('tokyonight', s.name) end) t.it('accepts a source key instead of the positional entry', function() local s = assert(spec.parse({ source = 'owner/repo', tag = 'v1' })) t.eq('owner/repo', s.source) end) t.it('rejects a bare string with no tag', function() local s, err = spec.parse('owner/repo') t.falsy(s) t.matches('missing a tag', err) end) t.it('rejects a source without an owner', function() local s, err = spec.parse({ 'plenary.nvim', tag = 'v1' }) t.falsy(s) t.matches('expected "owner/repo"', err) end) t.it('rejects a source with extra path segments', function() local s, err = spec.parse({ 'a/b/c', tag = 'v1' }) t.falsy(s) t.matches('expected "owner/repo"', err) end) t.it('rejects a tag containing a path separator', function() local s, err = spec.parse({ 'owner/repo', tag = '../../etc' }) t.falsy(s) t.matches('invalid tag', err) end) t.it('rejects an unsupported extension', function() local s, err = spec.parse({ 'owner/repo', tag = 'v1', ext = 'rar' }) t.falsy(s) t.matches('unsupported ext', err) end) t.it('rejects a traversing "as" name', function() local s, err = spec.parse({ 'owner/repo', tag = 'v1', as = '..' }) t.falsy(s) t.matches('unusable directory name', err) end) t.it('rejects a non-table, non-string spec', function() local s, err = spec.parse(42) t.falsy(s) t.matches('must be a string or a table', err) end) t.it('trims surrounding whitespace from the source and tag', function() local s = assert(spec.parse({ ' owner/repo ', tag = ' v1.0.0 ' })) t.eq('owner/repo', s.source) t.eq('v1.0.0', s.tag) end) end) t.describe('spec.url', function() t.it('builds a tag archive url', function() local s = assert(spec.parse({ 'nvim-lua/plenary.nvim', tag = 'v0.1.4' })) t.eq(REGISTRY .. '/nvim-lua/plenary.nvim/archive/refs/tags/v0.1.4.zip', spec.url(s, REGISTRY)) end) t.it('honours the tar.gz extension', function() local s = assert(spec.parse({ 'nvim-lua/plenary.nvim', tag = 'v0.1.4', ext = 'tar.gz' })) t.matches('/v0%.1%.4%.tar%.gz$', spec.url(s, REGISTRY)) end) t.it('does not double up on a trailing registry slash', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1' })) t.eq(REGISTRY .. '/owner/repo/archive/refs/tags/v1.zip', spec.url(s, REGISTRY .. '///')) end) t.it('passes an explicit url straight through', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1', url = 'https://example.test/a.zip' })) t.eq('https://example.test/a.zip', spec.url(s, REGISTRY)) end) t.it('errors when no registry is configured', function() local s = assert(spec.parse({ 'owner/repo', tag = 'v1' })) local url, err = spec.url(s, '') t.falsy(url) t.matches('registry', err) end) end) t.describe('spec.parse_all', function() t.it('collects specs and reports per-entry errors', function() local specs, errors = spec.parse_all({ { 'a/one', tag = 'v1' }, { 'bad-source', tag = 'v1' }, { 'b/two', tag = 'v2', opt = true }, }) t.eq(2, #specs) t.eq(1, #errors) t.matches('plugins%[2%]', errors[1]) end) t.it('rejects two specs that install to the same directory', function() local specs, errors = spec.parse_all({ { 'a/shared', tag = 'v1' }, { 'b/shared', tag = 'v2' }, }) t.eq(1, #specs) t.matches('duplicate plugin name', errors[1]) end) t.it('returns empty results for a nil plugin list', function() local specs, errors = spec.parse_all(nil) t.eq({}, specs) t.eq({}, errors) end) end)