neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

pack_spec.lua (85630B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 local Screen = require('test.functional.ui.screen')
      4 local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1'
      5 
      6 local api = n.api
      7 local fn = n.fn
      8 
      9 local eq = t.eq
     10 local matches = t.matches
     11 local pcall_err = t.pcall_err
     12 local exec_lua = n.exec_lua
     13 
     14 -- Helpers ====================================================================
     15 -- Installed plugins ----------------------------------------------------------
     16 
     17 local function pack_get_dir()
     18  return vim.fs.joinpath(fn.stdpath('data'), 'site', 'pack', 'core', 'opt')
     19 end
     20 
     21 local function pack_get_plug_path(plug_name)
     22  return vim.fs.joinpath(pack_get_dir(), plug_name)
     23 end
     24 
     25 local function pack_exists(plug_name)
     26  local path = vim.fs.joinpath(pack_get_dir(), plug_name)
     27  return vim.uv.fs_stat(path) ~= nil
     28 end
     29 
     30 --- Assert content from the main Lua file inside installed plugin.
     31 --- Used as a proxy for checking plugin state on disk.
     32 local function pack_assert_content(plug_name, content)
     33  local full_path = vim.fs.joinpath(pack_get_plug_path(plug_name), 'lua', plug_name .. '.lua')
     34  eq(content, fn.readblob(full_path))
     35 end
     36 
     37 -- Test repos (to be installed) -----------------------------------------------
     38 
     39 local repos_dir = vim.fs.abspath('test/functional/lua/pack-test-repos')
     40 
     41 --- Map from repo name to its proper `src` used in plugin spec
     42 --- @type table<string,string>
     43 local repos_src = {}
     44 
     45 local function repo_get_path(repo_name)
     46  vim.validate('repo_name', repo_name, 'string')
     47  return vim.fs.joinpath(repos_dir, repo_name)
     48 end
     49 
     50 local function repo_write_file(repo_name, rel_path, text, no_dedent, append)
     51  local path = vim.fs.joinpath(repo_get_path(repo_name), rel_path)
     52  fn.mkdir(vim.fs.dirname(path), 'p')
     53  t.write_file(path, text, no_dedent, append)
     54 end
     55 
     56 --- @return vim.SystemCompleted
     57 local function system_sync(cmd, opts)
     58  return exec_lua(function()
     59    local obj = vim.system(cmd, opts)
     60 
     61    if opts and opts.timeout then
     62      -- Minor delay before calling wait() so the timeout uv timer can have a headstart over the
     63      -- internal call to vim.wait() in wait().
     64      vim.wait(10)
     65    end
     66 
     67    local res = obj:wait()
     68 
     69    -- Check the process is no longer running
     70    assert(not vim.api.nvim_get_proc(obj.pid), 'process still exists')
     71 
     72    return res
     73  end)
     74 end
     75 
     76 local function git_cmd(cmd, repo_name)
     77  local git_cmd_prefix = {
     78    'git',
     79    '-c',
     80    'gc.auto=0',
     81    '-c',
     82    'user.name=Marvim',
     83    '-c',
     84    'user.email=marvim@neovim.io',
     85    '-c',
     86    'init.defaultBranch=main',
     87  }
     88 
     89  cmd = vim.list_extend(git_cmd_prefix, cmd)
     90  local cwd = repo_get_path(repo_name)
     91  local sys_opts = { cwd = cwd, text = true, clear_env = true }
     92  local out = system_sync(cmd, sys_opts)
     93  if out.code ~= 0 then
     94    error(out.stderr)
     95  end
     96  return (out.stdout:gsub('\n+$', ''))
     97 end
     98 
     99 local function init_test_repo(repo_name)
    100  local path = repo_get_path(repo_name)
    101  fn.mkdir(path, 'p')
    102  repos_src[repo_name] = 'file://' .. path
    103 
    104  git_cmd({ 'init' }, repo_name)
    105 end
    106 
    107 local function git_add_commit(msg, repo_name)
    108  git_cmd({ 'add', '*' }, repo_name)
    109  git_cmd({ 'commit', '-m', msg }, repo_name)
    110 end
    111 
    112 local function git_get_hash(rev, repo_name)
    113  return git_cmd({ 'rev-list', '-1', rev }, repo_name)
    114 end
    115 
    116 local function git_get_short_hash(rev, repo_name)
    117  return git_cmd({ 'rev-list', '-1', '--abbrev-commit', rev }, repo_name)
    118 end
    119 
    120 -- Common test repos ----------------------------------------------------------
    121 --- @type table<string,function>
    122 local repos_setup = {}
    123 
    124 function repos_setup.basic()
    125  init_test_repo('basic')
    126 
    127  repo_write_file('basic', 'lua/basic.lua', 'return "basic init"')
    128  git_add_commit('Initial commit for "basic"', 'basic')
    129  repo_write_file('basic', 'lua/basic.lua', 'return "basic main"')
    130  git_add_commit('Commit in `main` but not in `feat-branch`', 'basic')
    131 
    132  git_cmd({ 'checkout', 'main~' }, 'basic')
    133  git_cmd({ 'checkout', '-b', 'feat-branch' }, 'basic')
    134 
    135  repo_write_file('basic', 'lua/basic.lua', 'return "basic some-tag"')
    136  git_add_commit('Add commit for some tag', 'basic')
    137  git_cmd({ 'tag', 'some-tag' }, 'basic')
    138 
    139  repo_write_file('basic', 'lua/basic.lua', 'return "basic feat-branch"')
    140  git_add_commit('Add important feature', 'basic')
    141 
    142  -- Make sure that `main` is the default remote branch
    143  git_cmd({ 'checkout', 'main' }, 'basic')
    144 end
    145 
    146 function repos_setup.plugindirs()
    147  init_test_repo('plugindirs')
    148 
    149  -- Add semver tag
    150  repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs v0.0.1"')
    151  git_add_commit('Add version v0.0.1', 'plugindirs')
    152  git_cmd({ 'tag', 'v0.0.1' }, 'plugindirs')
    153 
    154  -- Add various 'plugin/' files
    155  repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs main"')
    156  repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true')
    157  repo_write_file('plugindirs', 'plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "p"')
    158  repo_write_file('plugindirs', 'plugin/dirs.vim', 'let g:_plugin_vim=v:true')
    159  repo_write_file('plugindirs', 'plugin/sub/dirs.lua', 'vim.g._plugin_sub = true')
    160  repo_write_file('plugindirs', 'plugin/bad % name.lua', 'vim.g._plugin_bad = true')
    161  repo_write_file('plugindirs', 'after/plugin/dirs.lua', 'vim.g._after_plugin = true')
    162  repo_write_file('plugindirs', 'after/plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "a"')
    163  repo_write_file('plugindirs', 'after/plugin/dirs.vim', 'let g:_after_plugin_vim=v:true')
    164  repo_write_file('plugindirs', 'after/plugin/sub/dirs.lua', 'vim.g._after_plugin_sub = true')
    165  repo_write_file('plugindirs', 'after/plugin/bad % name.lua', 'vim.g._after_plugin_bad = true')
    166  git_add_commit('Initial commit for "plugindirs"', 'plugindirs')
    167 end
    168 
    169 function repos_setup.helptags()
    170  init_test_repo('helptags')
    171  repo_write_file('helptags', 'lua/helptags.lua', 'return "helptags main"')
    172  repo_write_file('helptags', 'doc/my-test-help.txt', '*my-test-help*')
    173  repo_write_file('helptags', 'doc/bad % name.txt', '*my-test-help-bad*')
    174  repo_write_file('helptags', 'doc/bad % dir/file.txt', '*my-test-help-sub-bad*')
    175  git_add_commit('Initial commit for "helptags"', 'helptags')
    176 end
    177 
    178 function repos_setup.pluginerr()
    179  init_test_repo('pluginerr')
    180 
    181  repo_write_file('pluginerr', 'lua/pluginerr.lua', 'return "pluginerr main"')
    182  repo_write_file('pluginerr', 'plugin/err.lua', 'error("Wow, an error")')
    183  git_add_commit('Initial commit for "pluginerr"', 'pluginerr')
    184 end
    185 
    186 function repos_setup.defbranch()
    187  init_test_repo('defbranch')
    188 
    189  repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch main"')
    190  git_add_commit('Initial commit for "defbranch"', 'defbranch')
    191 
    192  -- Make `dev` the default remote branch
    193  git_cmd({ 'checkout', '-b', 'dev' }, 'defbranch')
    194 
    195  repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch dev"')
    196  git_add_commit('Add to new default branch', 'defbranch')
    197 end
    198 
    199 function repos_setup.gitsuffix()
    200  init_test_repo('gitsuffix.git')
    201 
    202  repo_write_file('gitsuffix.git', 'lua/gitsuffix.lua', 'return "gitsuffix main"')
    203  git_add_commit('Initial commit for "gitsuffix"', 'gitsuffix.git')
    204 end
    205 
    206 function repos_setup.semver()
    207  init_test_repo('semver')
    208 
    209  local function add_tag(name)
    210    repo_write_file('semver', 'lua/semver.lua', 'return "semver ' .. name .. '"')
    211    git_add_commit('Add version ' .. name, 'semver')
    212    git_cmd({ 'tag', name }, 'semver')
    213  end
    214 
    215  add_tag('v0.0.1')
    216  add_tag('v0.0.2')
    217  add_tag('v0.1.0')
    218  add_tag('v0.1.1')
    219  add_tag('v0.2.0-dev')
    220  add_tag('v0.2.0')
    221  add_tag('v0.3.0')
    222  repo_write_file('semver', 'lua/semver.lua', 'return "semver middle-commit')
    223  git_add_commit('Add middle commit', 'semver')
    224  add_tag('0.3.1')
    225  add_tag('v0.4')
    226  add_tag('non-semver')
    227  add_tag('v0.2.1') -- Intentionally add version not in order
    228  add_tag('v1.0.0')
    229 end
    230 
    231 function repos_setup.with_subs()
    232  -- To-be-submodule repo
    233  init_test_repo('sub')
    234 
    235  repo_write_file('sub', 'sub.lua', 'return "sub init"')
    236  git_add_commit('Initial commit for "sub"', 'sub')
    237 
    238  -- With-submodules repo with submodule recorded at its initial commit
    239  init_test_repo('with_subs')
    240 
    241  repo_write_file('with_subs', 'lua/with_subs.lua', 'return "with_subs init"')
    242  local sub_src = 'file://' .. repo_get_path('sub')
    243  git_cmd({ '-c', 'protocol.file.allow=always', 'submodule', 'add', sub_src, 'sub' }, 'with_subs')
    244  git_add_commit('Initial commit for "with_subs"', 'with_subs')
    245  git_cmd({ 'tag', 'init-commit' }, 'with_subs')
    246 
    247  -- Advance both submodule and with-submodules repos by one commit
    248  repo_write_file('sub', 'sub.lua', 'return "sub main"')
    249  git_add_commit('Second commit for "sub"', 'sub')
    250 
    251  repo_write_file('with_subs', 'lua/with_subs.lua', 'return "with_subs main"')
    252  git_cmd(
    253    { '-c', 'protocol.file.allow=always', 'submodule', 'update', '--remote', 'sub' },
    254    'with_subs'
    255  )
    256  git_add_commit('Second commit for "with_subs"', 'with_subs')
    257 end
    258 
    259 -- Utility --------------------------------------------------------------------
    260 
    261 --- Execute `vim.pack.add()` inside `testnvim` instance
    262 local function vim_pack_add(specs, opts)
    263  exec_lua(function()
    264    vim.pack.add(specs, opts)
    265  end)
    266 end
    267 
    268 local function watch_events(event)
    269  exec_lua(function()
    270    _G.event_log = _G.event_log or {} --- @type table[]
    271    vim.api.nvim_create_autocmd(event, {
    272      callback = function(ev)
    273        table.insert(_G.event_log, { event = ev.event, match = ev.match, data = ev.data })
    274      end,
    275    })
    276  end)
    277 end
    278 
    279 --- @param log table[]
    280 local function make_find_packchanged(log)
    281  --- @param suffix string
    282  return function(suffix, kind, repo_name, version, active)
    283    local path = pack_get_plug_path(repo_name)
    284    local spec = { name = repo_name, src = repos_src[repo_name], version = version }
    285    local data = { active = active, kind = kind, path = path, spec = spec }
    286    local entry = { event = 'PackChanged' .. suffix, match = vim.fs.abspath(path), data = data }
    287 
    288    local res = 0
    289    for i, tbl in ipairs(log) do
    290      if vim.deep_equal(tbl, entry) then
    291        res = i
    292        break
    293      end
    294    end
    295    eq(true, res > 0)
    296 
    297    return res
    298  end
    299 end
    300 
    301 local function track_nvim_echo()
    302  exec_lua(function()
    303    _G.echo_log = {}
    304    local nvim_echo_orig = vim.api.nvim_echo
    305    ---@diagnostic disable-next-line: duplicate-set-field
    306    vim.api.nvim_echo = function(...)
    307      table.insert(_G.echo_log, vim.deepcopy({ ... }))
    308      return nvim_echo_orig(...)
    309    end
    310  end)
    311 end
    312 
    313 local function assert_progress_report(action, step_names)
    314  -- NOTE: Assume that `nvim_echo` mocked log has only progress report messages
    315  local echo_log = exec_lua('return _G.echo_log') ---@type table[]
    316  local n_steps = #step_names
    317  eq(n_steps + 2, #echo_log)
    318 
    319  local progress = { kind = 'progress', title = 'vim.pack', status = 'running', percent = 0 }
    320  local init_step = { { { ('%s (0/%d)'):format(action, n_steps) } }, true, progress }
    321  eq(init_step, echo_log[1])
    322 
    323  local steps_seen = {} --- @type table<string,boolean>
    324  for i = 1, n_steps do
    325    local echo_args = echo_log[i + 1]
    326 
    327    -- NOTE: There is no guaranteed order (as it is async), so check that some
    328    -- expected step name is used in the message
    329    local msg = ('%s (%d/%d)'):format(action, i, n_steps)
    330    local pattern = '^' .. vim.pesc(msg) .. ' %- (%S+)$'
    331    local step = echo_args[1][1][1]:match(pattern) ---@type string
    332    eq(true, vim.tbl_contains(step_names, step))
    333    steps_seen[step] = true
    334 
    335    -- Should not add intermediate progress report to history
    336    eq(echo_args[2], false)
    337 
    338    -- Should update a single message by its id (computed after first call)
    339    progress.id = progress.id or echo_args[3].id ---@type integer
    340    progress.percent = math.floor(100 * i / n_steps)
    341    eq(echo_args[3], progress)
    342  end
    343 
    344  -- Should report all steps
    345  eq(n_steps, vim.tbl_count(steps_seen))
    346 
    347  progress.percent, progress.status = 100, 'success'
    348  local final_step = { { { ('%s (%d/%d)'):format(action, n_steps, n_steps) } }, true, progress }
    349  eq(final_step, echo_log[n_steps + 2])
    350 end
    351 
    352 local function mock_confirm(output_value)
    353  exec_lua(function()
    354    _G.confirm_log = _G.confirm_log or {}
    355 
    356    ---@diagnostic disable-next-line: duplicate-set-field
    357    vim.fn.confirm = function(...)
    358      table.insert(_G.confirm_log, { ... })
    359      return output_value
    360    end
    361  end)
    362 end
    363 
    364 local function mock_git_file_transport()
    365  -- HACK: mock `vim.system()` to have `git` commands be executed
    366  -- with temporarily set 'protocol.file.allow=always' option.
    367  -- Otherwise performing `git` operations with submodules from `vim.pack`
    368  -- itself will fail with `fatal: transport 'file' not allowed`.
    369  -- Directly adding `-c protocol.file.allow=always` to `git_cmd` in `vim.pack`
    370  -- itself is too much and might be bad for security.
    371  exec_lua(function()
    372    local vim_system_orig = vim.system
    373    vim.system = function(cmd, opts, on_exit)
    374      if cmd[1] == 'git' then
    375        table.insert(cmd, 2, '-c')
    376        table.insert(cmd, 3, 'protocol.file.allow=always')
    377      end
    378      return vim_system_orig(cmd, opts, on_exit)
    379    end
    380  end)
    381 end
    382 
    383 local function is_jit()
    384  return exec_lua('return package.loaded.jit ~= nil')
    385 end
    386 
    387 local function get_lock_path()
    388  return vim.fs.joinpath(fn.stdpath('config'), 'nvim-pack-lock.json')
    389 end
    390 
    391 --- @return {plugins:table<string, {rev:string, src:string, version?:string}>}
    392 local function get_lock_tbl()
    393  return vim.json.decode(fn.readblob(get_lock_path()))
    394 end
    395 
    396 -- Tests ======================================================================
    397 
    398 describe('vim.pack', function()
    399  setup(function()
    400    n.clear()
    401    for _, r_setup in pairs(repos_setup) do
    402      r_setup()
    403    end
    404  end)
    405 
    406  before_each(function()
    407    n.clear()
    408  end)
    409 
    410  after_each(function()
    411    vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
    412    vim.fs.rm(get_lock_path(), { force = true })
    413    local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
    414    pcall(vim.fs.rm, log_path, { force = true })
    415  end)
    416 
    417  teardown(function()
    418    vim.fs.rm(repos_dir, { force = true, recursive = true })
    419  end)
    420 
    421  describe('add()', function()
    422    it('installs only once', function()
    423      vim_pack_add({ repos_src.basic })
    424      n.clear()
    425 
    426      watch_events({ 'PackChanged' })
    427      vim_pack_add({ repos_src.basic })
    428      eq(exec_lua('return #_G.event_log'), 0)
    429 
    430      -- Should not create redundant stash entry
    431      local basic_path = pack_get_plug_path('basic')
    432      local stash_list = system_sync({ 'git', 'stash', 'list' }, { cwd = basic_path }).stdout or ''
    433      eq('', stash_list)
    434    end)
    435 
    436    it('passes `data` field through to `opts.load`', function()
    437      local out = exec_lua(function()
    438        local map = {} ---@type table<string,boolean>
    439        local function load(p)
    440          local name = p.spec.name ---@type string
    441          map[name] = name == 'basic' and (p.spec.data.test == 'value') or (p.spec.data == 'value')
    442        end
    443        vim.pack.add({
    444          { src = repos_src.basic, data = { test = 'value' } },
    445          { src = repos_src.defbranch, data = 'value' },
    446        }, { load = load })
    447        return map
    448      end)
    449      eq({ basic = true, defbranch = true }, out)
    450    end)
    451 
    452    it('asks for installation confirmation', function()
    453      -- Do not confirm installation to see what happens (should not error)
    454      mock_confirm(2)
    455 
    456      vim_pack_add({ repos_src.basic, { src = repos_src.defbranch, name = 'other-name' } })
    457      eq(false, pack_exists('basic'))
    458      eq(false, pack_exists('defbranch'))
    459      eq({ plugins = {} }, get_lock_tbl())
    460 
    461      local confirm_msg_lines = ([[
    462        These plugins will be installed:
    463 
    464        basic      from %s
    465        other-name from %s]]):format(repos_src.basic, repos_src.defbranch)
    466      local confirm_msg = vim.trim(vim.text.indent(0, confirm_msg_lines))
    467      local ref_log = { { confirm_msg .. '\n', 'Proceed? &Yes\n&No\n&Always', 1, 'Question' } }
    468      eq(ref_log, exec_lua('return _G.confirm_log'))
    469 
    470      -- Should remove lock data if not confirmed during lockfile sync
    471      n.clear()
    472      vim_pack_add({ repos_src.basic })
    473      eq(true, pack_exists('basic'))
    474      eq('table', type(get_lock_tbl().plugins.basic))
    475 
    476      vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
    477      n.clear()
    478      mock_confirm(2)
    479 
    480      vim_pack_add({ repos_src.basic })
    481      eq(false, pack_exists('basic'))
    482      eq({ plugins = {} }, get_lock_tbl())
    483 
    484      -- Should ask for confirm twice: during lockfile sync and inside
    485      -- `vim.pack.add()` (i.e. not confirming during lockfile sync has
    486      -- an immediate effect on whether a plugin is installed or not)
    487      eq(2, exec_lua('return #_G.confirm_log'))
    488    end)
    489 
    490    it('respects `opts.confirm`', function()
    491      mock_confirm(1)
    492      vim_pack_add({ repos_src.basic }, { confirm = false })
    493 
    494      eq(0, exec_lua('return #_G.confirm_log'))
    495      eq(true, pack_exists('basic'))
    496 
    497      -- Should also respect `confirm` when installing during lockfile sync
    498      vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
    499      eq('table', type(get_lock_tbl().plugins.basic))
    500 
    501      n.clear()
    502      mock_confirm(1)
    503 
    504      vim_pack_add({}, { confirm = false })
    505      eq(0, exec_lua('return #_G.confirm_log'))
    506      eq(true, pack_exists('basic'))
    507    end)
    508 
    509    it('can always confirm in current session', function()
    510      mock_confirm(3)
    511 
    512      vim_pack_add({ repos_src.basic })
    513      eq(1, exec_lua('return #_G.confirm_log'))
    514      eq('basic main', exec_lua('return require("basic")'))
    515 
    516      vim_pack_add({ repos_src.defbranch })
    517      eq(1, exec_lua('return #_G.confirm_log'))
    518      eq('defbranch dev', exec_lua('return require("defbranch")'))
    519 
    520      -- Should still ask in next session
    521      n.clear()
    522      mock_confirm(3)
    523      vim_pack_add({ repos_src.plugindirs })
    524      eq(1, exec_lua('return #_G.confirm_log'))
    525      eq('plugindirs main', exec_lua('return require("plugindirs")'))
    526    end)
    527 
    528    it('creates lockfile', function()
    529      local helptags_rev = git_get_hash('HEAD', 'helptags')
    530      exec_lua(function()
    531        vim.pack.add({
    532          { src = repos_src.basic, version = 'some-tag' },
    533          { src = repos_src.defbranch, version = 'main' },
    534          { src = repos_src.helptags, version = helptags_rev },
    535          { src = repos_src.plugindirs },
    536          { src = repos_src.semver, version = vim.version.range('*') },
    537        })
    538      end)
    539 
    540      local basic_rev = git_get_hash('some-tag', 'basic')
    541      local defbranch_rev = git_get_hash('main', 'defbranch')
    542      local plugindirs_rev = git_get_hash('HEAD', 'plugindirs')
    543      local semver_rev = git_get_hash('v1.0.0', 'semver')
    544 
    545      -- Should properly format as indented JSON. Notes:
    546      -- - Branch, tag, and commit should be serialized like `'value'` to be
    547      --   distinguishable from version ranges.
    548      -- - Absent `version` should be missing and not autoresolved.
    549      local ref_lockfile_lines = ([[
    550        {
    551          "plugins": {
    552            "basic": {
    553              "rev": "%s",
    554              "src": "%s",
    555              "version": "'some-tag'"
    556            },
    557            "defbranch": {
    558              "rev": "%s",
    559              "src": "%s",
    560              "version": "'main'"
    561            },
    562            "helptags": {
    563              "rev": "%s",
    564              "src": "%s",
    565              "version": "'%s'"
    566            },
    567            "plugindirs": {
    568              "rev": "%s",
    569              "src": "%s"
    570            },
    571            "semver": {
    572              "rev": "%s",
    573              "src": "%s",
    574              "version": ">=0.0.0"
    575            }
    576          }
    577        }]]):format(
    578        basic_rev,
    579        repos_src.basic,
    580        defbranch_rev,
    581        repos_src.defbranch,
    582        helptags_rev,
    583        repos_src.helptags,
    584        helptags_rev,
    585        plugindirs_rev,
    586        repos_src.plugindirs,
    587        semver_rev,
    588        repos_src.semver
    589      )
    590      eq(vim.text.indent(0, ref_lockfile_lines), fn.readblob(get_lock_path()))
    591    end)
    592 
    593    it('updates lockfile', function()
    594      vim_pack_add({ repos_src.basic })
    595      local ref_lockfile = {
    596        plugins = {
    597          basic = { rev = git_get_hash('main', 'basic'), src = repos_src.basic },
    598        },
    599      }
    600      eq(ref_lockfile, get_lock_tbl())
    601 
    602      n.clear()
    603      vim_pack_add({ { src = repos_src.basic, version = 'main' } })
    604 
    605      ref_lockfile.plugins.basic.version = "'main'"
    606      eq(ref_lockfile, get_lock_tbl())
    607    end)
    608 
    609    it('uses lockfile during install', function()
    610      vim_pack_add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch })
    611 
    612      -- Mock clean initial install, but with lockfile present
    613      vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
    614      n.clear()
    615      watch_events({ 'PackChangedPre', 'PackChanged' })
    616 
    617      local basic_rev = git_get_hash('feat-branch', 'basic')
    618      local defbranch_rev = git_get_hash('HEAD', 'defbranch')
    619      local ref_lockfile = {
    620        plugins = {
    621          basic = { rev = basic_rev, src = repos_src.basic, version = "'feat-branch'" },
    622          defbranch = { rev = defbranch_rev, src = repos_src.defbranch },
    623        },
    624      }
    625      eq(ref_lockfile, get_lock_tbl())
    626 
    627      mock_confirm(1)
    628      -- Should use revision from lockfile (pointing at latest 'feat-branch'
    629      -- commit) and not use latest `main` commit
    630      vim_pack_add({ { src = repos_src.basic, version = 'main' } })
    631      pack_assert_content('basic', 'return "basic feat-branch"')
    632 
    633      local confirm_log = exec_lua('return _G.confirm_log')
    634      eq(1, #confirm_log)
    635      matches('basic.*defbranch', confirm_log[1][1])
    636 
    637      -- Should install `defbranch` (as it is in lockfile), but not load it
    638      eq(true, pack_exists('defbranch'))
    639      eq(false, exec_lua('return pcall(require, "defbranch")'))
    640 
    641      -- Should trigger `kind=install` events
    642      local log = exec_lua('return _G.event_log')
    643      local find_event = make_find_packchanged(log)
    644      local installpre_basic = find_event('Pre', 'install', 'basic', 'feat-branch', false)
    645      local installpre_defbranch = find_event('Pre', 'install', 'defbranch', nil, false)
    646      local install_basic = find_event('', 'install', 'basic', 'feat-branch', false)
    647      local install_defbranch = find_event('', 'install', 'defbranch', nil, false)
    648      eq(4, #log)
    649      eq(true, installpre_basic < install_basic)
    650      eq(true, installpre_defbranch < install_defbranch)
    651 
    652      -- Running `update()` should still update to use `main`
    653      exec_lua(function()
    654        vim.pack.update({ 'basic' }, { force = true })
    655      end)
    656      pack_assert_content('basic', 'return "basic main"')
    657 
    658      ref_lockfile.plugins.basic.rev = git_get_hash('main', 'basic')
    659      ref_lockfile.plugins.basic.version = "'main'"
    660      eq(ref_lockfile, get_lock_tbl())
    661    end)
    662 
    663    it('handles lockfile during install errors', function()
    664      local repo_not_exist = 'file://' .. repo_get_path('does-not-exist')
    665      pcall_err(exec_lua, function()
    666        vim.pack.add({
    667          repo_not_exist,
    668          { src = repos_src.basic, version = 'not-exist' },
    669          { src = repos_src.pluginerr, version = 'main' },
    670        })
    671      end)
    672 
    673      local pluginerr_hash = git_get_hash('main', 'pluginerr')
    674      local ref_lockfile = {
    675        -- Should be no entry for `repo_not_exist` and `basic` as they did not
    676        -- fully install
    677        plugins = {
    678          -- Error during sourcing 'plugin/' should not affect lockfile
    679          pluginerr = { rev = pluginerr_hash, src = repos_src.pluginerr, version = "'main'" },
    680        },
    681      }
    682      eq(ref_lockfile, get_lock_tbl())
    683    end)
    684 
    685    it('regenerates manually deleted lockfile', function()
    686      vim_pack_add({
    687        { src = repos_src.basic, name = 'other', version = 'feat-branch' },
    688        repos_src.defbranch,
    689      })
    690      local lock_path = get_lock_path()
    691      eq(true, vim.uv.fs_stat(lock_path) ~= nil)
    692 
    693      local basic_rev = git_get_hash('feat-branch', 'basic')
    694      local plugindirs_rev = git_get_hash('dev', 'defbranch')
    695 
    696      -- Should try its best to regenerate lockfile based on installed plugins
    697      fn.delete(get_lock_path())
    698      n.clear()
    699      vim_pack_add({})
    700      local ref_lockfile = {
    701        plugins = {
    702          -- No `version = 'feat-branch'` as there is no way to get that info
    703          -- (lockfile was the only source of that on disk)
    704          other = { rev = basic_rev, src = repos_src.basic },
    705          defbranch = { rev = plugindirs_rev, src = repos_src.defbranch },
    706        },
    707      }
    708      eq(ref_lockfile, get_lock_tbl())
    709 
    710      local ref_messages = 'vim.pack: Repaired corrupted lock data for plugins: defbranch, other'
    711      eq(ref_messages, n.exec_capture('messages'))
    712 
    713      -- Calling `add()` with `version` should still add it to lockfile
    714      vim_pack_add({ { src = repos_src.basic, name = 'other', version = 'feat-branch' } })
    715      eq("'feat-branch'", get_lock_tbl().plugins.other.version)
    716    end)
    717 
    718    it('repairs corrupted lock data for installed plugins', function()
    719      vim_pack_add({
    720        -- Should preserve present `version`
    721        { src = repos_src.basic, version = 'feat-branch' },
    722        repos_src.defbranch,
    723        repos_src.semver,
    724        repos_src.helptags,
    725      })
    726 
    727      local lock_tbl = get_lock_tbl()
    728      local ref_lock_tbl = vim.deepcopy(lock_tbl)
    729      local assert = function()
    730        vim_pack_add({})
    731        eq(ref_lock_tbl, get_lock_tbl())
    732        eq(true, pack_exists('basic'))
    733        eq(true, pack_exists('defbranch'))
    734        eq(true, pack_exists('semver'))
    735        eq(true, pack_exists('helptags'))
    736      end
    737 
    738      -- Missing lock data required field
    739      lock_tbl.plugins.basic.rev = nil
    740      -- Wrong lock data field type
    741      lock_tbl.plugins.defbranch.src = 1 ---@diagnostic disable-line: assign-type-mismatch
    742      -- Wrong lock data type
    743      lock_tbl.plugins.semver = 1 ---@diagnostic disable-line: assign-type-mismatch
    744 
    745      local lockfile_text = vim.json.encode(lock_tbl, { indent = '  ', sort_keys = true })
    746      fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path())
    747 
    748      n.clear()
    749      assert()
    750 
    751      local ref_messages =
    752        'vim.pack: Repaired corrupted lock data for plugins: basic, defbranch, semver'
    753      eq(ref_messages, n.exec_capture('messages'))
    754 
    755      -- Should work even for badly corrupted lockfile
    756      lockfile_text = vim.json.encode({ plugins = 1 }, { indent = '  ', sort_keys = true })
    757      fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path())
    758 
    759      n.clear()
    760      -- Can not preserve `version` if it was deleted from the lockfile
    761      ref_lock_tbl.plugins.basic.version = nil
    762      assert()
    763    end)
    764 
    765    it('removes unrepairable corrupted data and plugins', function()
    766      vim_pack_add({ repos_src.basic, repos_src.defbranch, repos_src.semver, repos_src.helptags })
    767 
    768      local lock_tbl = get_lock_tbl()
    769      local ref_lock_tbl = vim.deepcopy(lock_tbl)
    770 
    771      -- Corrupted data for missing plugin
    772      vim.fs.rm(pack_get_plug_path('basic'), { recursive = true, force = true })
    773      lock_tbl.plugins.basic.rev = nil
    774 
    775      -- Good data for corrupted plugin
    776      local defbranch_path = pack_get_plug_path('defbranch')
    777      vim.fs.rm(defbranch_path, { recursive = true, force = true })
    778      fn.writefile({ 'File and not directory' }, defbranch_path)
    779 
    780      -- Corrupted data for corrupted plugin
    781      local semver_path = pack_get_plug_path('semver')
    782      vim.fs.rm(semver_path, { recursive = true, force = true })
    783      fn.writefile({ 'File and not directory' }, semver_path)
    784      lock_tbl.plugins.semver.rev = 1 ---@diagnostic disable-line: assign-type-mismatch
    785 
    786      local lockfile_text = vim.json.encode(lock_tbl, { indent = '  ', sort_keys = true })
    787      fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path())
    788 
    789      n.clear()
    790      vim_pack_add({})
    791      ref_lock_tbl.plugins.basic = nil
    792      ref_lock_tbl.plugins.defbranch = nil
    793      ref_lock_tbl.plugins.semver = nil
    794      eq(ref_lock_tbl, get_lock_tbl())
    795 
    796      eq(false, pack_exists('basic'))
    797      eq(false, pack_exists('defbranch'))
    798      eq(false, pack_exists('semver'))
    799      eq(true, pack_exists('helptags'))
    800    end)
    801 
    802    it('installs at proper version', function()
    803      local out = exec_lua(function()
    804        vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } })
    805        -- Should have plugin available immediately (not even on the next loop)
    806        return require('basic')
    807      end)
    808 
    809      eq('basic feat-branch', out)
    810 
    811      local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths())
    812      local plug_path = pack_get_plug_path('basic')
    813      local after_dir = vim.fs.joinpath(plug_path, 'after')
    814      eq(true, vim.tbl_contains(rtp, plug_path))
    815      -- No 'after/' directory in runtimepath because it is not present in plugin
    816      eq(false, vim.tbl_contains(rtp, after_dir))
    817    end)
    818 
    819    it('installs with submodules', function()
    820      mock_git_file_transport()
    821      vim_pack_add({ repos_src.with_subs })
    822 
    823      local sub_lua_file = vim.fs.joinpath(pack_get_plug_path('with_subs'), 'sub', 'sub.lua')
    824      eq('return "sub main"', fn.readblob(sub_lua_file))
    825    end)
    826 
    827    it('does not install on bad `version`', function()
    828      local err = pcall_err(exec_lua, function()
    829        vim.pack.add({ { src = repos_src.basic, version = 'not-exist' } })
    830      end)
    831      matches('`not%-exist` is not a branch/tag/commit', err)
    832      eq(false, pack_exists('basic'))
    833    end)
    834 
    835    it('can install from the Internet', function()
    836      t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
    837      vim_pack_add({ 'https://github.com/neovim/nvim-lspconfig' })
    838      eq(true, exec_lua('return pcall(require, "lspconfig")'))
    839    end)
    840 
    841    describe('startup', function()
    842      local config_dir, pack_add_cmd = '', ''
    843 
    844      before_each(function()
    845        config_dir = fn.stdpath('config')
    846        fn.mkdir(vim.fs.joinpath(config_dir, 'plugin'), 'p')
    847 
    848        pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs))
    849      end)
    850 
    851      after_each(function()
    852        vim.fs.rm(config_dir, { recursive = true, force = true })
    853      end)
    854 
    855      local function assert_loaded()
    856        eq('plugindirs main', exec_lua('return require("plugindirs")'))
    857 
    858        -- Should source 'plugin/' and 'after/plugin/' exactly once
    859        eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
    860        eq({ 'p', 'a' }, n.exec_lua('return _G.DL'))
    861      end
    862 
    863      local function assert_works()
    864        -- Should auto-install but wait before executing code after it
    865        n.clear({ args_rm = { '-u' } })
    866        t.retry(nil, 2000, function()
    867          eq(true, exec_lua('return _G.done'))
    868        end)
    869        assert_loaded()
    870 
    871        -- Should only `:packadd!`/`:packadd` already installed plugin
    872        n.clear({ args_rm = { '-u' } })
    873        assert_loaded()
    874      end
    875 
    876      it('works in init.lua', function()
    877        local init_lua = vim.fs.joinpath(config_dir, 'init.lua')
    878        fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua)
    879        assert_works()
    880 
    881        -- Should not load plugins if `--noplugin`, only adjust 'runtimepath'
    882        n.clear({ args = { '--noplugin' }, args_rm = { '-u' } })
    883        eq('plugindirs main', exec_lua('return require("plugindirs")'))
    884        eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
    885        eq(vim.NIL, n.exec_lua('return _G.DL'))
    886      end)
    887 
    888      it('works in plugin/', function()
    889        local plugin_file = vim.fs.joinpath(config_dir, 'plugin', 'mine.lua')
    890        fn.writefile({ pack_add_cmd, '_G.done = true' }, plugin_file)
    891        -- Should source plugin's 'plugin/' files without explicit `load=true`
    892        assert_works()
    893      end)
    894    end)
    895 
    896    it('shows progress report during installation', function()
    897      track_nvim_echo()
    898      vim_pack_add({ repos_src.basic, repos_src.defbranch })
    899      assert_progress_report('Installing plugins', { 'basic', 'defbranch' })
    900    end)
    901 
    902    it('triggers relevant events', function()
    903      watch_events({ 'PackChangedPre', 'PackChanged' })
    904 
    905      -- Should provide event-data respecting manual `version` without inferring default
    906      vim_pack_add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch })
    907 
    908      local log = exec_lua('return _G.event_log')
    909      local find_event = make_find_packchanged(log)
    910      local installpre_basic = find_event('Pre', 'install', 'basic', 'feat-branch', false)
    911      local installpre_defbranch = find_event('Pre', 'install', 'defbranch', nil, false)
    912      local install_basic = find_event('', 'install', 'basic', 'feat-branch', false)
    913      local install_defbranch = find_event('', 'install', 'defbranch', nil, false)
    914      eq(4, #log)
    915 
    916      -- NOTE: There is no guaranteed installation order among separate plugins (as it is async)
    917      eq(true, installpre_basic < install_basic)
    918      eq(true, installpre_defbranch < install_defbranch)
    919    end)
    920 
    921    it('recognizes several `version` types', function()
    922      local prev_commit = git_get_hash('HEAD~', 'defbranch')
    923      exec_lua(function()
    924        vim.pack.add({
    925          { src = repos_src.basic, version = 'some-tag' }, -- Tag
    926          { src = repos_src.defbranch, version = prev_commit }, -- Commit hash
    927          { src = repos_src.semver, version = vim.version.range('<1') }, -- Semver constraint
    928        })
    929      end)
    930 
    931      eq('basic some-tag', exec_lua('return require("basic")'))
    932      eq('defbranch main', exec_lua('return require("defbranch")'))
    933      eq('semver v0.4', exec_lua('return require("semver")'))
    934    end)
    935 
    936    it('respects plugin/ and after/plugin/ scripts', function()
    937      local function assert(load, ref)
    938        vim_pack_add({ { src = repos_src.plugindirs, name = 'plugin % dirs' } }, { load = load })
    939        -- Should handle bad plugin directory name
    940        local out = exec_lua(function()
    941          return {
    942            vim.g._plugin,
    943            vim.g._plugin_vim,
    944            vim.g._plugin_sub,
    945            vim.g._plugin_bad,
    946            vim.g._after_plugin,
    947            vim.g._after_plugin_vim,
    948            vim.g._after_plugin_sub,
    949            vim.g._after_plugin_bad,
    950          }
    951        end)
    952        eq(ref, out)
    953 
    954        -- Should add necessary directories to runtimepath regardless of `opts.load`
    955        local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths())
    956        local plug_path = pack_get_plug_path('plugin % dirs')
    957        local after_dir = vim.fs.joinpath(plug_path, 'after')
    958        eq(true, vim.tbl_contains(rtp, plug_path))
    959        eq(true, vim.tbl_contains(rtp, after_dir))
    960      end
    961 
    962      assert(nil, { true, true, true, true, true, true, true, true })
    963 
    964      n.clear()
    965      assert(false, {})
    966    end)
    967 
    968    it('can use function `opts.load`', function()
    969      local function assert()
    970        n.exec_lua(function()
    971          _G.load_log = {}
    972          local function load(...)
    973            table.insert(_G.load_log, { ... })
    974          end
    975          vim.pack.add({ repos_src.plugindirs, repos_src.basic }, { load = load })
    976        end)
    977 
    978        -- Order of execution should be the same as supplied in `add()`
    979        local plugindirs_data = {
    980          spec = { src = repos_src.plugindirs, name = 'plugindirs' },
    981          path = pack_get_plug_path('plugindirs'),
    982        }
    983        local basic_data = {
    984          spec = { src = repos_src.basic, name = 'basic' },
    985          path = pack_get_plug_path('basic'),
    986        }
    987        -- - Only single table argument should be supplied to `load`
    988        local ref_log = { { plugindirs_data }, { basic_data } }
    989        eq(ref_log, n.exec_lua('return _G.load_log'))
    990 
    991        -- Should not add plugin to the session in any way
    992        eq(false, exec_lua('return pcall(require, "plugindirs")'))
    993        eq(false, exec_lua('return pcall(require, "basic")'))
    994 
    995        -- Should not source 'plugin/'
    996        eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
    997 
    998        -- Plugins should still be marked as "active", since they were added
    999        eq(true, exec_lua('return vim.pack.get({ "plugindirs" })[1].active'))
   1000        eq(true, exec_lua('return vim.pack.get({ "basic" })[1].active'))
   1001      end
   1002 
   1003      -- Works on initial install
   1004      assert()
   1005 
   1006      -- Works when loading already installed plugin
   1007      n.clear()
   1008      assert()
   1009    end)
   1010 
   1011    it('generates help tags', function()
   1012      vim_pack_add({ { src = repos_src.helptags, name = 'help tags' } })
   1013      local target_tags = fn.getcompletion('my-test', 'help')
   1014      table.sort(target_tags)
   1015      eq({ 'my-test-help', 'my-test-help-bad', 'my-test-help-sub-bad' }, target_tags)
   1016    end)
   1017 
   1018    it('reports install/load errors after loading all input', function()
   1019      t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'")
   1020      local function assert(err_pat)
   1021        local err = pcall_err(exec_lua, function()
   1022          vim.pack.add({
   1023            { src = repos_src.basic, version = 'wrong-version' }, -- Error during initial checkout
   1024            { src = repos_src.semver, version = vim.version.range('>=2.0.0') }, -- Missing version
   1025            { src = repos_src.plugindirs, version = 'main' },
   1026            { src = repos_src.pluginerr, version = 'main' }, -- Error during 'plugin/' source
   1027          })
   1028        end)
   1029 
   1030        matches(err_pat, err)
   1031 
   1032        -- Should have processed non-errored 'plugin/' and add to 'rtp'
   1033        eq('plugindirs main', exec_lua('return require("plugindirs")'))
   1034        eq(true, exec_lua('return vim.g._plugin'))
   1035 
   1036        -- Should add plugin to 'rtp' even if 'plugin/' has error
   1037        eq('pluginerr main', exec_lua('return require("pluginerr")'))
   1038      end
   1039 
   1040      -- During initial install
   1041      local err_pat_parts = {
   1042        'vim%.pack',
   1043        '`basic`:\n',
   1044        -- Should report available branches and tags if revision is absent
   1045        '`wrong%-version`',
   1046        -- Should list default branch first
   1047        'Available:\nTags: some%-tag\nBranches: main, feat%-branch',
   1048        -- Should report available branches and versions if no constraint match
   1049        '`semver`',
   1050        'Available:\nVersions: v1%.0%.0, v0%.4, 0%.3%.1, v0%.3%.0.*\nBranches: main\n',
   1051        '`pluginerr`:\n',
   1052        'Wow, an error',
   1053      }
   1054      assert(table.concat(err_pat_parts, '.*'))
   1055 
   1056      -- During loading already installed plugin.
   1057      n.clear()
   1058      -- NOTE: There is no error for wrong `version`, because there is no check
   1059      -- for already installed plugins. Might change in the future.
   1060      assert('vim%.pack.*`pluginerr`:\n.*Wow, an error')
   1061    end)
   1062 
   1063    it('normalizes each spec', function()
   1064      vim_pack_add({
   1065        repos_src.basic, -- String should be inferred as `{ src = ... }`
   1066        { src = repos_src.defbranch }, -- Default `version` is remote's default branch
   1067        { src = repos_src['gitsuffix.git'] }, -- Default `name` comes from `src` repo name
   1068        { src = repos_src.plugindirs, name = 'plugin/dirs' }, -- Ensure proper directory name
   1069      })
   1070 
   1071      eq('basic main', exec_lua('return require("basic")'))
   1072      eq('defbranch dev', exec_lua('return require("defbranch")'))
   1073      eq('gitsuffix main', exec_lua('return require("gitsuffix")'))
   1074      eq(true, exec_lua('return vim.g._plugin'))
   1075 
   1076      eq(true, pack_exists('gitsuffix'))
   1077      eq(true, pack_exists('dirs'))
   1078    end)
   1079 
   1080    it('handles problematic names', function()
   1081      vim_pack_add({ { src = repos_src.basic, name = 'bad % name' } })
   1082      eq('basic main', exec_lua('return require("basic")'))
   1083    end)
   1084 
   1085    it('is not affected by special environment variables', function()
   1086      fn.setenv('GIT_WORK_TREE', t.paths.test_source_path)
   1087      fn.setenv('GIT_DIR', vim.fs.joinpath(t.paths.test_source_path, '.git'))
   1088      local ref_environ = fn.environ()
   1089 
   1090      vim_pack_add({ repos_src.basic })
   1091      eq('basic main', exec_lua('return require("basic")'))
   1092 
   1093      eq(ref_environ, fn.environ())
   1094    end)
   1095 
   1096    it('validates input', function()
   1097      local function assert(err_pat, input)
   1098        local function add_input()
   1099          vim.pack.add(input)
   1100        end
   1101        matches(err_pat, pcall_err(exec_lua, add_input))
   1102      end
   1103 
   1104      -- Separate spec entries
   1105      assert('list', repos_src.basic)
   1106      assert('spec:.*table', { 1 })
   1107      assert('spec%.src:.*string', { { src = 1 } })
   1108      assert('spec%.src:.*non%-empty string', { { src = '' } })
   1109      assert('spec%.name:.*string', { { src = repos_src.basic, name = 1 } })
   1110      assert('spec%.name:.*non%-empty string', { { src = repos_src.basic, name = '' } })
   1111      assert(
   1112        'spec%.version:.*string or vim%.VersionRange',
   1113        { { src = repos_src.basic, version = 1 } }
   1114      )
   1115 
   1116      -- Conflicts in input array
   1117      local version_conflict = {
   1118        { src = repos_src.basic, version = 'feat-branch' },
   1119        { src = repos_src.basic, version = 'main' },
   1120      }
   1121      assert('Conflicting `version` for `basic`.*feat%-branch.*main', version_conflict)
   1122 
   1123      local src_conflict = {
   1124        { src = repos_src.basic, name = 'my-plugin' },
   1125        { src = repos_src.semver, name = 'my-plugin' },
   1126      }
   1127      assert('Conflicting `src` for `my%-plugin`.*basic.*semver', src_conflict)
   1128    end)
   1129  end)
   1130 
   1131  describe('update()', function()
   1132    -- Tables with hashes used to test confirmation buffer and log content
   1133    local hashes --- @type table<string,string>
   1134    local short_hashes --- @type table<string,string>
   1135 
   1136    before_each(function()
   1137      -- Create a dedicated clean repo for which "push changes" will be mocked
   1138      init_test_repo('fetch')
   1139 
   1140      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch init"')
   1141      git_add_commit('Initial commit for "fetch"', 'fetch')
   1142 
   1143      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch main"')
   1144      git_add_commit('Commit from `main` to be removed', 'fetch')
   1145 
   1146      hashes = { fetch_head = git_get_hash('HEAD', 'fetch') }
   1147      short_hashes = { fetch_head = git_get_short_hash('HEAD', 'fetch') }
   1148 
   1149      -- Install initial versions of tested plugins
   1150      vim_pack_add({
   1151        { src = repos_src.fetch, version = 'main' },
   1152        { src = repos_src.semver, version = 'v0.3.0' },
   1153        repos_src.defbranch,
   1154      })
   1155      n.clear()
   1156 
   1157      -- Mock remote repo update
   1158      -- - Force push
   1159      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new"')
   1160      git_cmd({ 'add', '*' }, 'fetch')
   1161      git_cmd({ 'commit', '--amend', '-m', 'Commit to be added 1' }, 'fetch')
   1162 
   1163      -- - Presence of a tag (should be shown in changelog)
   1164      git_cmd({ 'tag', 'dev-tag' }, 'fetch')
   1165 
   1166      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"')
   1167      git_add_commit('Commit to be added 2', 'fetch')
   1168 
   1169      -- Make `dev` default remote branch to check that `version` is respected
   1170      git_cmd({ 'checkout', '-b', 'dev' }, 'fetch')
   1171      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch dev"')
   1172      git_add_commit('Commit from default `dev` branch', 'fetch')
   1173    end)
   1174 
   1175    after_each(function()
   1176      pcall(vim.fs.rm, repo_get_path('fetch'), { force = true, recursive = true })
   1177      local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
   1178      pcall(vim.fs.rm, log_path, { force = true })
   1179    end)
   1180 
   1181    describe('confirmation buffer', function()
   1182      it('works', function()
   1183        vim_pack_add({
   1184          repos_src.fetch,
   1185          { src = repos_src.semver, version = 'v0.3.0' },
   1186          { src = repos_src.defbranch, version = 'does-not-exist' },
   1187        })
   1188        pack_assert_content('fetch', 'return "fetch main"')
   1189 
   1190        exec_lua(function()
   1191          -- Enable highlighting of special filetype
   1192          vim.cmd('filetype plugin on')
   1193          vim.pack.update()
   1194        end)
   1195 
   1196        -- Buffer should be special and shown in a separate tabpage
   1197        eq(2, #api.nvim_list_tabpages())
   1198        eq(2, fn.tabpagenr())
   1199        eq(api.nvim_get_option_value('filetype', {}), 'nvim-pack')
   1200        eq(api.nvim_get_option_value('modifiable', {}), false)
   1201        eq(api.nvim_get_option_value('buftype', {}), 'acwrite')
   1202        local confirm_bufnr = api.nvim_get_current_buf()
   1203        local confirm_winnr = api.nvim_get_current_win()
   1204        local confirm_tabpage = api.nvim_get_current_tabpage()
   1205        eq(api.nvim_buf_get_name(0), 'nvim-pack://confirm#' .. confirm_bufnr)
   1206 
   1207        -- Adjust lines for a more robust screenshot testing
   1208        local fetch_src = repos_src.fetch
   1209        local fetch_path = pack_get_plug_path('fetch')
   1210        local semver_src = repos_src.semver
   1211        local semver_path = pack_get_plug_path('semver')
   1212        local pack_runtime = '/lua/vim/pack.lua'
   1213 
   1214        exec_lua(function()
   1215          -- Replace matches in line to preserve extmark highlighting
   1216          local function replace_in_line(i, pattern, repl)
   1217            local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1]
   1218            local from, to = line:find(pattern)
   1219            while from and to do
   1220              vim.api.nvim_buf_set_text(0, i - 1, from - 1, i - 1, to, { repl })
   1221              line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1]
   1222              from, to = line:find(pattern)
   1223            end
   1224          end
   1225 
   1226          vim.bo.modifiable = true
   1227          local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
   1228          -- NOTE: replace path to `vim.pack` in error traceback accounting for
   1229          -- pcall source truncation and possibly different slashes on Windows
   1230          local pack_runtime_pesc = vim.pesc(pack_runtime):gsub('/', '[\\/]')
   1231          local pack_runtime_pattern = ('%%S.+%s:%%d+'):format(pack_runtime_pesc)
   1232          for i = 1, #lines do
   1233            replace_in_line(i, pack_runtime_pattern, 'VIM_PACK_RUNTIME')
   1234            replace_in_line(i, vim.pesc(fetch_path), 'FETCH_PATH')
   1235            replace_in_line(i, vim.pesc(fetch_src), 'FETCH_SRC')
   1236            replace_in_line(i, vim.pesc(semver_path), 'SEMVER_PATH')
   1237            replace_in_line(i, vim.pesc(semver_src), 'SEMVER_SRC')
   1238          end
   1239          vim.bo.modified = false
   1240          vim.bo.modifiable = false
   1241        end)
   1242 
   1243        -- Use screenshot to test highlighting, otherwise prefer text matching.
   1244        -- This requires computing target hashes on each test run because they
   1245        -- change due to source repos being cleanly created on each file test.
   1246        local screen
   1247        screen = Screen.new(85, 35)
   1248 
   1249        hashes.fetch_new = git_get_hash('main', 'fetch')
   1250        short_hashes.fetch_new = git_get_short_hash('main', 'fetch')
   1251        short_hashes.fetch_new_prev = git_get_short_hash('main~', 'fetch')
   1252        hashes.semver_head = git_get_hash('v0.3.0', 'semver')
   1253 
   1254        local tab_name = 'n' .. (t.is_os('win') and ':' or '') .. '//confirm#2'
   1255 
   1256        local ref_screen_lines = ([[
   1257          {24: [No Name] }{5: %s }{2:%s                                                          }{24:X}|
   1258          {19:^# Error ────────────────────────────────────────────────────────────────────────}     |
   1259                                                                                               |
   1260          {19:## defbranch}                                                                         |
   1261                                                                                               |
   1262           VIM_PACK_RUNTIME: `does-not-exist` is not a branch/tag/commit. Available:           |
   1263            Tags:                                                                              |
   1264            Branches: dev, main                                                                |
   1265                                                                                               |
   1266          {101:# Update ───────────────────────────────────────────────────────────────────────}     |
   1267                                                                                               |
   1268          {101:## fetch}                                                                             |
   1269          Path:            {103:FETCH_PATH}                                                          |
   1270          Source:          {103:FETCH_SRC}                                                           |
   1271          Revision before: {103:%s}                            |
   1272          Revision after:  {103:%s} {102:(main)}                     |
   1273                                                                                               |
   1274          Pending updates:                                                                     |
   1275          {19:< %s │ Commit from `main` to be removed}                                         |
   1276          {104:> %s │ Commit to be added 2}                                                     |
   1277          {104:> %s │ Commit to be added 1 (tag: dev-tag)}                                      |
   1278                                                                                               |
   1279          {102:# Same ─────────────────────────────────────────────────────────────────────────}     |
   1280                                                                                               |
   1281          {102:## semver}                                                                            |
   1282          Path:     {103:SEMVER_PATH}                                                                |
   1283          Source:   {103:SEMVER_SRC}                                                                 |
   1284          Revision: {103:%s} {102:(v0.3.0)}                          |
   1285                                                                                               |
   1286          Available newer versions:                                                            |
   1287          • {102:v1.0.0}                                                                             |
   1288          • {102:v0.4}                                                                               |
   1289          • {102:0.3.1}                                                                              |
   1290          {1:~                                                                                    }|
   1291                                                                                               |
   1292        ]]):format(
   1293          tab_name,
   1294          t.is_os('win') and '' or ' ',
   1295          hashes.fetch_head,
   1296          hashes.fetch_new,
   1297          short_hashes.fetch_head,
   1298          short_hashes.fetch_new,
   1299          short_hashes.fetch_new_prev,
   1300          hashes.semver_head
   1301        )
   1302 
   1303        screen:add_extra_attr_ids({
   1304          [101] = { foreground = Screen.colors.Orange },
   1305          [102] = { foreground = Screen.colors.LightGray },
   1306          [103] = { foreground = Screen.colors.LightBlue },
   1307          [104] = { foreground = Screen.colors.SeaGreen },
   1308        })
   1309        -- NOTE: Non LuaJIT reports errors differently due to 'coxpcall'
   1310        if is_jit() then
   1311          local ref_screen = vim.text.indent(0, ref_screen_lines)
   1312          screen:expect(ref_screen)
   1313        end
   1314 
   1315        -- `:write` should confirm
   1316        n.exec('write')
   1317 
   1318        -- - Apply changes immediately
   1319        pack_assert_content('fetch', 'return "fetch new 2"')
   1320 
   1321        -- - Clean up buffer+window+tabpage
   1322        eq(false, api.nvim_buf_is_valid(confirm_bufnr))
   1323        eq(false, api.nvim_win_is_valid(confirm_winnr))
   1324        eq(false, api.nvim_tabpage_is_valid(confirm_tabpage))
   1325 
   1326        -- - Write to log file
   1327        local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
   1328        local log_text = fn.readblob(log_path)
   1329        local log_1, log_rest = log_text:match('^(.-)\n(.*)$') --- @type string, string
   1330        matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_1)
   1331        local ref_log_lines = ([[
   1332          # Update ───────────────────────────────────────────────────────────────────────
   1333 
   1334          ## fetch
   1335          Path:            %s
   1336          Source:          %s
   1337          Revision before: %s
   1338          Revision after:  %s (main)
   1339 
   1340          Pending updates:
   1341          < %s │ Commit from `main` to be removed
   1342          > %s │ Commit to be added 2
   1343          > %s │ Commit to be added 1 (tag: dev-tag)]]):format(
   1344          fetch_path,
   1345          fetch_src,
   1346          hashes.fetch_head,
   1347          hashes.fetch_new,
   1348          short_hashes.fetch_head,
   1349          short_hashes.fetch_new,
   1350          short_hashes.fetch_new_prev
   1351        )
   1352        eq(vim.text.indent(0, ref_log_lines), vim.trim(log_rest))
   1353      end)
   1354 
   1355      it('can be dismissed with `:quit`', function()
   1356        vim_pack_add({ repos_src.fetch })
   1357        exec_lua('vim.pack.update({ "fetch" })')
   1358        eq('nvim-pack', api.nvim_get_option_value('filetype', {}))
   1359 
   1360        -- Should not apply updates
   1361        n.exec('quit')
   1362        pack_assert_content('fetch', 'return "fetch main"')
   1363      end)
   1364 
   1365      it('closes full tabpage', function()
   1366        vim_pack_add({ repos_src.fetch })
   1367        exec_lua('vim.pack.update()')
   1368 
   1369        -- Confirm with `:write`
   1370        local confirm_tabpage = api.nvim_get_current_tabpage()
   1371        n.exec('-tab split other-tab')
   1372        local other_tabpage = api.nvim_get_current_tabpage()
   1373        n.exec('tabnext')
   1374        n.exec('write')
   1375        eq(true, api.nvim_get_current_tabpage() == other_tabpage)
   1376        eq(false, api.nvim_tabpage_is_valid(confirm_tabpage))
   1377 
   1378        -- Not confirm with `:quit`
   1379        n.exec('tab split other-tab-2')
   1380        local other_tabpage_2 = api.nvim_get_current_tabpage()
   1381        exec_lua('vim.pack.update()')
   1382        confirm_tabpage = api.nvim_get_current_tabpage()
   1383 
   1384        -- - Temporary split window in tabpage should prevent from closing
   1385        n.exec('vsplit other-buf')
   1386        n.exec('wincmd w')
   1387 
   1388        n.exec('tabclose ' .. api.nvim_tabpage_get_number(other_tabpage_2))
   1389        eq(confirm_tabpage, api.nvim_get_current_tabpage())
   1390        n.exec('quit')
   1391        eq(confirm_tabpage, api.nvim_get_current_tabpage())
   1392        n.exec('quit')
   1393        eq(false, api.nvim_tabpage_is_valid(confirm_tabpage))
   1394 
   1395        -- Should work even if it is the last tabpage
   1396        exec_lua('vim.pack.update()')
   1397        n.exec('tabonly')
   1398        n.exec('write')
   1399        eq('', n.eval('v:errmsg'))
   1400 
   1401        -- Should cleanly close tabpage even if there are only scratch buffers
   1402        n.exec('%bwipeout')
   1403        local init_buf = api.nvim_get_current_buf()
   1404        api.nvim_set_current_buf(api.nvim_create_buf(false, true))
   1405        api.nvim_buf_delete(init_buf, { force = true })
   1406        exec_lua('vim.pack.update()')
   1407        n.exec('write')
   1408        eq(1, #api.nvim_list_tabpages())
   1409        eq(1, #api.nvim_list_bufs())
   1410      end)
   1411 
   1412      it('has in-process LSP features', function()
   1413        t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'")
   1414        track_nvim_echo()
   1415        vim_pack_add({
   1416          repos_src.fetch,
   1417          -- No `semver` to test with non-active plugins
   1418          { src = repos_src.defbranch, version = 'does-not-exist' },
   1419        })
   1420        exec_lua('vim.pack.update()')
   1421 
   1422        eq(1, exec_lua('return #vim.lsp.get_clients({ bufnr = 0 })'))
   1423 
   1424        -- textDocument/documentSymbol
   1425        exec_lua('vim.lsp.buf.document_symbol()')
   1426        local loclist = vim.tbl_map(function(x) --- @param x table
   1427          return {
   1428            lnum = x.lnum, --- @type integer
   1429            col = x.col, --- @type integer
   1430            end_lnum = x.end_lnum, --- @type integer
   1431            end_col = x.end_col, --- @type integer
   1432            text = x.text, --- @type string
   1433          }
   1434        end, fn.getloclist(0))
   1435        local ref_loclist = {
   1436          { lnum = 1, col = 1, end_lnum = 9, end_col = 1, text = '[Namespace] Error' },
   1437          { lnum = 3, col = 1, end_lnum = 9, end_col = 1, text = '[Module] defbranch' },
   1438          { lnum = 9, col = 1, end_lnum = 22, end_col = 1, text = '[Namespace] Update' },
   1439          { lnum = 11, col = 1, end_lnum = 22, end_col = 1, text = '[Module] fetch' },
   1440          { lnum = 22, col = 1, end_lnum = 32, end_col = 1, text = '[Namespace] Same' },
   1441          { lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver (not active)' },
   1442        }
   1443        eq(ref_loclist, loclist)
   1444 
   1445        n.exec('lclose')
   1446 
   1447        -- textDocument/hover
   1448        local confirm_winnr = api.nvim_get_current_win()
   1449        local function assert_hover(pos, commit_msg)
   1450          api.nvim_win_set_cursor(0, pos)
   1451          exec_lua(function()
   1452            vim.lsp.buf.hover()
   1453            -- Default hover is async shown in floating window
   1454            vim.wait(1000, function()
   1455              return #vim.api.nvim_tabpage_list_wins(0) > 1
   1456            end)
   1457          end)
   1458 
   1459          local all_wins = api.nvim_tabpage_list_wins(0)
   1460          eq(2, #all_wins)
   1461          local float_winnr = all_wins[1] == confirm_winnr and all_wins[2] or all_wins[1]
   1462          eq(true, api.nvim_win_get_config(float_winnr).relative ~= '')
   1463 
   1464          local float_buf = api.nvim_win_get_buf(float_winnr)
   1465          local text = table.concat(api.nvim_buf_get_lines(float_buf, 0, -1, false), '\n')
   1466 
   1467          local ref_pattern = 'Marvim <marvim@neovim%.io>\nDate:.*' .. vim.pesc(commit_msg)
   1468          matches(ref_pattern, text)
   1469        end
   1470 
   1471        assert_hover({ 14, 0 }, 'Commit from `main` to be removed')
   1472        assert_hover({ 15, 0 }, 'Commit to be added 2')
   1473        assert_hover({ 18, 0 }, 'Commit from `main` to be removed')
   1474        assert_hover({ 19, 0 }, 'Commit to be added 2')
   1475        assert_hover({ 20, 0 }, 'Commit to be added 1')
   1476        assert_hover({ 27, 0 }, 'Add version v0.3.0')
   1477        assert_hover({ 30, 0 }, 'Add version v1.0.0')
   1478        assert_hover({ 31, 0 }, 'Add version v0.4')
   1479        assert_hover({ 32, 0 }, 'Add version 0.3.1')
   1480 
   1481        -- textDocument/codeAction
   1482        n.exec_lua(function()
   1483          -- Mock `vim.ui.select()` which is a default code action selection
   1484          _G.select_idx = 0
   1485 
   1486          ---@diagnostic disable-next-line: duplicate-set-field
   1487          vim.ui.select = function(items, _, on_choice)
   1488            _G.select_items = items
   1489            local idx = _G.select_idx
   1490            if idx > 0 then
   1491              on_choice(items[idx], idx)
   1492              -- Minor delay before continue because LSP cmd execution is async
   1493              vim.wait(10)
   1494            end
   1495          end
   1496        end)
   1497 
   1498        local ref_lockfile = get_lock_tbl() --- @type vim.pack.Lock
   1499 
   1500        local function assert_action(pos, action_titles, select_idx)
   1501          api.nvim_win_set_cursor(0, pos)
   1502 
   1503          local lines = api.nvim_buf_get_lines(0, 0, -1, false)
   1504          n.exec_lua(function()
   1505            _G.select_items = nil
   1506            _G.select_idx = select_idx
   1507            vim.lsp.buf.code_action()
   1508          end)
   1509          local titles = vim.tbl_map(function(x) --- @param x table
   1510            return x.action.title
   1511          end, n.exec_lua('return _G.select_items or {}'))
   1512          eq(titles, action_titles)
   1513 
   1514          -- If no action is asked (like via cancel), should not delete lines
   1515          if select_idx <= 0 then
   1516            eq(lines, api.nvim_buf_get_lines(0, 0, -1, false))
   1517          end
   1518        end
   1519 
   1520        -- - Should not include "namespace" header as "plugin at cursor"
   1521        assert_action({ 1, 1 }, {}, 0)
   1522        assert_action({ 2, 0 }, {}, 0)
   1523        -- - No actions for `defbranch` since it is active and has no updates
   1524        assert_action({ 3, 1 }, {}, 0)
   1525        assert_action({ 7, 0 }, {}, 0)
   1526        -- - Should not include separator blank line as "plugin at cursor"
   1527        assert_action({ 8, 0 }, {}, 0)
   1528        assert_action({ 9, 0 }, {}, 0)
   1529        assert_action({ 10, 0 }, {}, 0)
   1530        -- - Should suggest updating related actions if updates available
   1531        local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`' }
   1532        assert_action({ 11, 0 }, fetch_actions, 0)
   1533        assert_action({ 14, 0 }, fetch_actions, 0)
   1534        assert_action({ 20, 0 }, fetch_actions, 0)
   1535        assert_action({ 21, 0 }, {}, 0)
   1536        assert_action({ 22, 0 }, {}, 0)
   1537        assert_action({ 23, 0 }, {}, 0)
   1538        -- - Only deletion should be available for not active plugins
   1539        assert_action({ 24, 0 }, { 'Delete `semver`' }, 0)
   1540        assert_action({ 28, 0 }, { 'Delete `semver`' }, 0)
   1541        assert_action({ 32, 0 }, { 'Delete `semver`' }, 0)
   1542 
   1543        -- - Should correctly perform action and remove plugin's lines
   1544        local function line_match(lnum, pattern)
   1545          matches(pattern, api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1])
   1546        end
   1547 
   1548        -- - Delete not active plugin. Should remove from disk and update lockfile.
   1549        assert_action({ 24, 0 }, { 'Delete `semver`' }, 1)
   1550        eq(false, pack_exists('semver'))
   1551        line_match(22, '^# Same')
   1552        eq(22, api.nvim_buf_line_count(0))
   1553 
   1554        ref_lockfile.plugins.semver = nil
   1555        eq(ref_lockfile, get_lock_tbl())
   1556 
   1557        -- - Skip udating
   1558        assert_action({ 11, 0 }, fetch_actions, 2)
   1559        pack_assert_content('fetch', 'return "fetch main"')
   1560        line_match(9, '^# Update')
   1561        line_match(10, '^$')
   1562        line_match(11, '^# Same')
   1563 
   1564        -- - Update plugin. Should not re-fetch new data and update lockfile.
   1565        n.exec('quit')
   1566        n.exec_lua(function()
   1567          vim.pack.update({ 'fetch' })
   1568        end)
   1569        exec_lua('_G.echo_log = {}')
   1570 
   1571        ref_lockfile.plugins.fetch.rev = git_get_hash('main', 'fetch')
   1572        repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"')
   1573        git_add_commit('Commit to be added 3', 'fetch')
   1574 
   1575        assert_action({ 3, 0 }, fetch_actions, 1)
   1576 
   1577        pack_assert_content('fetch', 'return "fetch new 2"')
   1578        assert_progress_report('Applying updates', { 'fetch' })
   1579        line_match(1, '^# Update')
   1580        eq(1, api.nvim_buf_line_count(0))
   1581 
   1582        eq(ref_lockfile, get_lock_tbl())
   1583 
   1584        -- - Can still respect `:write` after action
   1585        n.exec('write')
   1586        eq('vim.pack: Nothing to update', n.exec_capture('1messages'))
   1587        eq(api.nvim_get_option_value('filetype', {}), '')
   1588      end)
   1589 
   1590      it('has buffer-local mappings', function()
   1591        t.skip(not is_jit(), "Non LuaJIT reports update errors differently due to 'coxpcall'")
   1592        vim_pack_add({
   1593          repos_src.fetch,
   1594          { src = repos_src.semver, version = 'v0.3.0' },
   1595          { src = repos_src.defbranch, version = 'does-not-exist' },
   1596        })
   1597        -- Enable sourcing filetype script (that creates mappings)
   1598        n.exec('filetype plugin on')
   1599        exec_lua('vim.pack.update()')
   1600 
   1601        -- Plugin sections navigation
   1602        local function assert(keys, ref_cursor)
   1603          n.feed(keys)
   1604          eq(ref_cursor, api.nvim_win_get_cursor(0))
   1605        end
   1606 
   1607        api.nvim_win_set_cursor(0, { 1, 1 })
   1608        assert(']]', { 3, 0 })
   1609        assert(']]', { 11, 0 })
   1610        assert(']]', { 24, 0 })
   1611        -- - Should not wrap around the edge
   1612        assert(']]', { 24, 0 })
   1613 
   1614        api.nvim_win_set_cursor(0, { 32, 1 })
   1615        assert('[[', { 24, 0 })
   1616        assert('[[', { 11, 0 })
   1617        assert('[[', { 3, 0 })
   1618        -- - Should not wrap around the edge
   1619        assert('[[', { 3, 0 })
   1620      end)
   1621 
   1622      it('suggests newer versions when on non-tagged commit', function()
   1623        local commit = git_get_hash('0.3.1~', 'semver')
   1624 
   1625        -- Make fresh install for cleaner test
   1626        exec_lua('vim.pack.del({ "semver" })')
   1627        vim_pack_add({ { src = repos_src.semver, version = commit } })
   1628        exec_lua('vim.pack.update({ "semver" })')
   1629 
   1630        -- Should correctly infer that 0.3.0 is the latest version and suggest
   1631        -- versions greater than that
   1632        local confirm_text = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), '\n')
   1633        matches('Available newer versions:\n• v1%.0%.0\n• v0%.4\n• 0%.3%.1$', confirm_text)
   1634      end)
   1635 
   1636      it('updates lockfile', function()
   1637        vim_pack_add({ repos_src.fetch })
   1638        local ref_fetch_lock = { rev = hashes.fetch_head, src = repos_src.fetch }
   1639        eq(ref_fetch_lock, get_lock_tbl().plugins.fetch)
   1640 
   1641        exec_lua('vim.pack.update()')
   1642        n.exec('write')
   1643 
   1644        ref_fetch_lock.rev = git_get_hash('main', 'fetch')
   1645        eq(ref_fetch_lock, get_lock_tbl().plugins.fetch)
   1646      end)
   1647 
   1648      it('hints about not active plugins', function()
   1649        exec_lua(function()
   1650          vim.pack.update()
   1651        end)
   1652 
   1653        for _, l in ipairs(api.nvim_buf_get_lines(0, 0, -1, false)) do
   1654          if l:match('^## ') then
   1655            matches(' %(not active%)$', l)
   1656          end
   1657        end
   1658 
   1659        -- Should also hint in `textDocument/documentSymbol` of in-process LSP,
   1660        -- yet still work for navigation
   1661        exec_lua('vim.lsp.buf.document_symbol()')
   1662        local loclist = fn.getloclist(0)
   1663        matches(' %(not active%)$', loclist[2].text)
   1664        matches(' %(not active%)$', loclist[4].text)
   1665        matches(' %(not active%)$', loclist[5].text)
   1666 
   1667        n.exec('llast')
   1668        eq(21, api.nvim_win_get_cursor(0)[1])
   1669      end)
   1670    end)
   1671 
   1672    it('works with not active plugins', function()
   1673      -- No plugins are added, but they are installed in `before_each()`
   1674      exec_lua(function()
   1675        -- By default should also include not active plugins
   1676        vim.pack.update()
   1677      end)
   1678      pack_assert_content('fetch', 'return "fetch main"')
   1679      n.exec('write')
   1680      pack_assert_content('fetch', 'return "fetch new 2"')
   1681    end)
   1682 
   1683    it('works with submodules', function()
   1684      mock_git_file_transport()
   1685      vim_pack_add({ { src = repos_src.with_subs, version = 'init-commit' } })
   1686 
   1687      local sub_lua_file = vim.fs.joinpath(pack_get_plug_path('with_subs'), 'sub', 'sub.lua')
   1688      eq('return "sub init"', fn.readblob(sub_lua_file))
   1689 
   1690      n.clear()
   1691      mock_git_file_transport()
   1692      vim_pack_add({ repos_src.with_subs })
   1693      exec_lua('vim.pack.update({ "with_subs" })')
   1694      n.exec('write')
   1695      eq('return "sub main"', fn.readblob(sub_lua_file))
   1696    end)
   1697 
   1698    it('can force update', function()
   1699      vim_pack_add({ repos_src.fetch })
   1700      exec_lua('vim.pack.update({ "fetch" }, { force = true })')
   1701 
   1702      -- Apply changes immediately
   1703      local fetch_src = repos_src.fetch
   1704      local fetch_path = pack_get_plug_path('fetch')
   1705      pack_assert_content('fetch', 'return "fetch new 2"')
   1706 
   1707      -- No special buffer/window/tabpage
   1708      eq(1, #api.nvim_list_tabpages())
   1709      eq(1, #api.nvim_list_wins())
   1710      eq('', api.nvim_get_option_value('filetype', {}))
   1711 
   1712      -- Write to log file
   1713      hashes.fetch_new = git_get_hash('main', 'fetch')
   1714      short_hashes.fetch_new = git_get_short_hash('main', 'fetch')
   1715      short_hashes.fetch_new_prev = git_get_short_hash('main~', 'fetch')
   1716 
   1717      local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
   1718      local log_text = fn.readblob(log_path)
   1719      local log_1, log_rest = log_text:match('^(.-)\n(.*)$') --- @type string, string
   1720      matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_1)
   1721      local ref_log_lines = ([[
   1722        # Update ───────────────────────────────────────────────────────────────────────
   1723 
   1724        ## fetch
   1725        Path:            %s
   1726        Source:          %s
   1727        Revision before: %s
   1728        Revision after:  %s (main)
   1729 
   1730        Pending updates:
   1731        < %s │ Commit from `main` to be removed
   1732        > %s │ Commit to be added 2
   1733        > %s │ Commit to be added 1 (tag: dev-tag)]]):format(
   1734        fetch_path,
   1735        fetch_src,
   1736        hashes.fetch_head,
   1737        hashes.fetch_new,
   1738        short_hashes.fetch_head,
   1739        short_hashes.fetch_new,
   1740        short_hashes.fetch_new_prev
   1741      )
   1742      eq(vim.text.indent(0, ref_log_lines), vim.trim(log_rest))
   1743 
   1744      -- Should update lockfile
   1745      eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
   1746    end)
   1747 
   1748    it('can use lockfile revision as a target', function()
   1749      vim_pack_add({ repos_src.fetch })
   1750      pack_assert_content('fetch', 'return "fetch main"')
   1751 
   1752      -- Mock "update -> revert lockfile -> revert plugin"
   1753      local lock_path = get_lock_path()
   1754      local lockfile_before = fn.readblob(lock_path)
   1755      hashes.fetch_new = git_get_hash('main', 'fetch')
   1756 
   1757      -- - Update
   1758      exec_lua('vim.pack.update({ "fetch" }, { force = true })')
   1759      pack_assert_content('fetch', 'return "fetch new 2"')
   1760 
   1761      -- - Revert lockfile
   1762      fn.writefile(vim.split(lockfile_before, '\n'), lock_path)
   1763      n.clear()
   1764 
   1765      -- - Revert plugin
   1766      pack_assert_content('fetch', 'return "fetch new 2"')
   1767      exec_lua('vim.pack.update({ "fetch" }, { target = "lockfile" })')
   1768      local confirm_lines = api.nvim_buf_get_lines(0, 0, -1, false)
   1769      n.exec('write')
   1770      pack_assert_content('fetch', 'return "fetch main"')
   1771      eq(hashes.fetch_head, get_lock_tbl().plugins.fetch.rev)
   1772 
   1773      -- - Should mention that new revision comes from *lockfile*
   1774      eq(confirm_lines[6], ('Revision before: %s'):format(hashes.fetch_new))
   1775      eq(confirm_lines[7], ('Revision after:  %s (*lockfile*)'):format(hashes.fetch_head))
   1776    end)
   1777 
   1778    it('can change `src` of installed plugin', function()
   1779      local basic_src = repos_src.basic
   1780      local defbranch_src = repos_src.defbranch
   1781      vim_pack_add({ basic_src })
   1782 
   1783      local function assert_origin(ref)
   1784        -- Should be in sync both on disk and in lockfile
   1785        local opts = { cwd = pack_get_plug_path('basic') }
   1786        local real_origin = system_sync({ 'git', 'remote', 'get-url', 'origin' }, opts)
   1787        eq(ref, vim.trim(real_origin.stdout))
   1788 
   1789        eq(ref, get_lock_tbl().plugins.basic.src)
   1790      end
   1791 
   1792      n.clear()
   1793      watch_events({ 'PackChangedPre', 'PackChanged' })
   1794 
   1795      assert_origin(basic_src)
   1796      vim_pack_add({ { src = defbranch_src, name = 'basic' } })
   1797      -- Should not yet (after `add()`) affect plugin source
   1798      assert_origin(basic_src)
   1799 
   1800      -- Should update source immediately (to work if updates are discarded)
   1801      exec_lua(function()
   1802        vim.pack.update({ 'basic' })
   1803      end)
   1804      assert_origin(defbranch_src)
   1805 
   1806      -- Should not revert source change even if update is discarded
   1807      n.exec('quit')
   1808      assert_origin(defbranch_src)
   1809      eq({}, exec_lua('return _G.event_log'))
   1810 
   1811      -- Should work with forced update
   1812      n.clear()
   1813      vim_pack_add({ basic_src })
   1814      exec_lua('vim.pack.update({ "basic" }, { force = true })')
   1815      assert_origin(basic_src)
   1816    end)
   1817 
   1818    it('can do offline update', function()
   1819      vim_pack_add({ { src = repos_src.defbranch, version = 'main' } })
   1820      track_nvim_echo()
   1821 
   1822      pack_assert_content('defbranch', 'return "defbranch dev"')
   1823      n.exec_lua(function()
   1824        vim.pack.update({ 'defbranch' }, { offline = true })
   1825      end)
   1826 
   1827      -- There should be no progress report about downloading updates
   1828      assert_progress_report('Computing updates', { 'defbranch' })
   1829 
   1830      n.exec('write')
   1831      pack_assert_content('defbranch', 'return "defbranch main"')
   1832    end)
   1833 
   1834    it('shows progress report', function()
   1835      track_nvim_echo()
   1836      vim_pack_add({ repos_src.fetch, repos_src.defbranch })
   1837      -- Should also include updates from not active plugins
   1838      exec_lua('vim.pack.update()')
   1839 
   1840      -- During initial download
   1841      assert_progress_report('Downloading updates', { 'fetch', 'defbranch', 'semver' })
   1842      exec_lua('_G.echo_log = {}')
   1843 
   1844      -- During application (only for plugins that have updates)
   1845      n.exec('write')
   1846      assert_progress_report('Applying updates', { 'fetch' })
   1847 
   1848      -- During force update
   1849      n.clear()
   1850      track_nvim_echo()
   1851      repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"')
   1852      git_add_commit('Commit to be added 3', 'fetch')
   1853 
   1854      vim_pack_add({ repos_src.fetch, repos_src.defbranch })
   1855      exec_lua('vim.pack.update(nil, { force = true })')
   1856      assert_progress_report('Updating', { 'fetch', 'defbranch', 'semver' })
   1857    end)
   1858 
   1859    it('triggers relevant events', function()
   1860      watch_events({ 'PackChangedPre', 'PackChanged' })
   1861      vim_pack_add({ repos_src.fetch, repos_src.defbranch })
   1862      exec_lua('_G.event_log = {}')
   1863      exec_lua('vim.pack.update()')
   1864      eq({}, exec_lua('return _G.event_log'))
   1865 
   1866      -- Should trigger relevant events only for actually updated plugins
   1867      n.exec('write')
   1868      local log = exec_lua('return _G.event_log')
   1869      local find_event = make_find_packchanged(log)
   1870      eq(1, find_event('Pre', 'update', 'fetch', nil, true))
   1871      eq(2, find_event('', 'update', 'fetch', nil, true))
   1872      eq(2, #log)
   1873    end)
   1874 
   1875    it('stashes before applying changes', function()
   1876      local fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua')
   1877      fn.writefile({ 'A text that will be stashed' }, fetch_lua_file)
   1878 
   1879      vim_pack_add({ repos_src.fetch })
   1880      exec_lua('vim.pack.update()')
   1881      n.exec('write')
   1882 
   1883      local fetch_path = pack_get_plug_path('fetch')
   1884      local stash_list = system_sync({ 'git', 'stash', 'list' }, { cwd = fetch_path }).stdout or ''
   1885      matches('vim%.pack: %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d Stash before checkout', stash_list)
   1886 
   1887      -- Update should still be applied
   1888      pack_assert_content('fetch', 'return "fetch new 2"')
   1889    end)
   1890 
   1891    it('is not affected by special environment variables', function()
   1892      fn.setenv('GIT_WORK_TREE', t.paths.test_source_path)
   1893      fn.setenv('GIT_DIR', vim.fs.joinpath(t.paths.test_source_path, '.git'))
   1894      local ref_environ = fn.environ()
   1895 
   1896      vim_pack_add({ repos_src.fetch })
   1897      exec_lua('vim.pack.update({ "fetch" }, { force = true })')
   1898      pack_assert_content('fetch', 'return "fetch new 2"')
   1899 
   1900      eq(ref_environ, fn.environ())
   1901    end)
   1902 
   1903    it('works with out of sync lockfile', function()
   1904      -- Should first autoinstall missing plugin (with confirmation)
   1905      vim.fs.rm(pack_get_plug_path('fetch'), { force = true, recursive = true })
   1906      n.clear()
   1907      mock_confirm(1)
   1908      exec_lua(function()
   1909        vim.pack.update(nil, { force = true })
   1910      end)
   1911      eq(1, exec_lua('return #_G.confirm_log'))
   1912      -- - Should checkout `version='main'` as it says in the lockfile
   1913      pack_assert_content('fetch', 'return "fetch new 2"')
   1914 
   1915      -- Should regenerate absent lockfile (from present plugins)
   1916      vim.fs.rm(get_lock_path())
   1917      n.clear()
   1918      exec_lua(function()
   1919        vim.pack.update(nil, { force = true })
   1920      end)
   1921      local lock_plugins = get_lock_tbl().plugins
   1922      eq(3, vim.tbl_count(lock_plugins))
   1923      -- - Should checkout default branch since `version='main'` info is lost
   1924      --   after lockfile is deleted.
   1925      eq(nil, lock_plugins.fetch.version)
   1926      pack_assert_content('fetch', 'return "fetch dev"')
   1927    end)
   1928 
   1929    it('validates input', function()
   1930      local function assert(err_pat, input)
   1931        local function update_input()
   1932          vim.pack.update(input)
   1933        end
   1934        matches(err_pat, pcall_err(exec_lua, update_input))
   1935      end
   1936 
   1937      assert('list', 1)
   1938 
   1939      -- Should first check if every plugin name represents installed plugin
   1940      -- If not - stop early before any update
   1941      vim_pack_add({ repos_src.basic })
   1942 
   1943      assert('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' })
   1944 
   1945      -- Empty list is allowed with warning
   1946      n.exec('messages clear')
   1947      exec_lua(function()
   1948        vim.pack.update({})
   1949      end)
   1950      eq('vim.pack: Nothing to update', n.exec_capture('messages'))
   1951    end)
   1952  end)
   1953 
   1954  describe('get()', function()
   1955    local function make_basic_data(active, info)
   1956      local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' }
   1957      local path = pack_get_plug_path('basic')
   1958      local rev = git_get_hash('feat-branch', 'basic')
   1959      local res = { active = active, path = path, spec = spec, rev = rev }
   1960      if info then
   1961        res.branches = { 'main', 'feat-branch' }
   1962        res.tags = { 'some-tag' }
   1963      end
   1964      return res
   1965    end
   1966 
   1967    local function make_defbranch_data(active, info)
   1968      local spec = { name = 'defbranch', src = repos_src.defbranch }
   1969      local path = pack_get_plug_path('defbranch')
   1970      local rev = git_get_hash('dev', 'defbranch')
   1971      local res = { active = active, path = path, spec = spec, rev = rev }
   1972      if info then
   1973        res.branches = { 'dev', 'main' }
   1974        res.tags = {}
   1975      end
   1976      return res
   1977    end
   1978 
   1979    local function make_plugindirs_data(active, info)
   1980      local spec =
   1981        { name = 'plugindirs', src = repos_src.plugindirs, version = vim.version.range('*') }
   1982      local path = pack_get_plug_path('plugindirs')
   1983      local rev = git_get_hash('v0.0.1', 'plugindirs')
   1984      local res = { active = active, path = path, spec = spec, rev = rev }
   1985      if info then
   1986        res.branches = { 'main' }
   1987        res.tags = { 'v0.0.1' }
   1988      end
   1989      return res
   1990    end
   1991 
   1992    it('returns list with necessary data', function()
   1993      local basic_data, defbranch_data, plugindirs_data
   1994 
   1995      -- Should work just after installation
   1996      exec_lua(function()
   1997        vim.pack.add({
   1998          repos_src.defbranch,
   1999          { src = repos_src.basic, version = 'feat-branch' },
   2000          { src = repos_src.plugindirs, version = vim.version.range('*') },
   2001        })
   2002      end)
   2003      defbranch_data = make_defbranch_data(true, true)
   2004      basic_data = make_basic_data(true, true)
   2005      plugindirs_data = make_plugindirs_data(true, true)
   2006      -- Should preserve order in which plugins were `vim.pack.add()`ed
   2007      eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()'))
   2008 
   2009      -- Should also list non-active plugins
   2010      n.clear()
   2011      vim_pack_add({ repos_src.defbranch })
   2012      defbranch_data = make_defbranch_data(true, true)
   2013      basic_data = make_basic_data(false, true)
   2014      plugindirs_data = make_plugindirs_data(false, true)
   2015 
   2016      -- Should first list active, then non-active (including their latest
   2017      -- set `version` which is inferred from lockfile)
   2018      eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()'))
   2019 
   2020      -- Should respect `names` for both active and not active plugins
   2021      eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" })'))
   2022      eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" })'))
   2023      eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get({ "basic", "defbranch" })'))
   2024 
   2025      local bad_get_cmd = 'return vim.pack.get({ "ccc", "basic", "aaa" })'
   2026      matches('Plugin `ccc` is not installed', pcall_err(exec_lua, bad_get_cmd))
   2027 
   2028      -- Should respect `opts.info`
   2029      defbranch_data = make_defbranch_data(true, false)
   2030      basic_data = make_basic_data(false, false)
   2031      plugindirs_data = make_plugindirs_data(false, false)
   2032      eq(
   2033        { defbranch_data, basic_data, plugindirs_data },
   2034        exec_lua('return vim.pack.get(nil, { info = false })')
   2035      )
   2036      eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })'))
   2037      eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })'))
   2038    end)
   2039 
   2040    it('respects `data` field', function()
   2041      vim_pack_add({
   2042        { src = repos_src.basic, version = 'feat-branch', data = { test = 'value' } },
   2043        { src = repos_src.defbranch, data = 'value' },
   2044      })
   2045      local out = exec_lua(function()
   2046        local plugs = vim.pack.get()
   2047        ---@type table<string,string>
   2048        return { basic = plugs[1].spec.data.test, defbranch = plugs[2].spec.data }
   2049      end)
   2050      eq({ basic = 'value', defbranch = 'value' }, out)
   2051    end)
   2052 
   2053    it('works with `del()`', function()
   2054      vim_pack_add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } })
   2055 
   2056      exec_lua(function()
   2057        _G.get_log = {}
   2058        vim.api.nvim_create_autocmd({ 'PackChangedPre', 'PackChanged' }, {
   2059          callback = function()
   2060            table.insert(_G.get_log, vim.pack.get())
   2061          end,
   2062        })
   2063      end)
   2064 
   2065      -- Should not include removed plugins immediately after they are removed,
   2066      -- while still returning list without holes
   2067      exec_lua('vim.pack.del({ "defbranch" }, { force = true })')
   2068      local defbranch_data = make_defbranch_data(true, true)
   2069      local basic_data = make_basic_data(true, true)
   2070      eq({ { defbranch_data, basic_data }, { basic_data } }, exec_lua('return _G.get_log'))
   2071    end)
   2072 
   2073    it('works with out of sync lockfile', function()
   2074      vim_pack_add({ repos_src.basic, repos_src.defbranch })
   2075      eq(2, vim.tbl_count(get_lock_tbl().plugins))
   2076 
   2077      -- Should first autoinstall missing plugin (with confirmation)
   2078      vim.fs.rm(pack_get_plug_path('basic'), { force = true, recursive = true })
   2079      n.clear()
   2080      mock_confirm(1)
   2081      eq(2, exec_lua('return #vim.pack.get()'))
   2082 
   2083      eq(1, exec_lua('return #_G.confirm_log'))
   2084      pack_assert_content('basic', 'return "basic main"')
   2085 
   2086      -- Should regenerate absent lockfile (from present plugins)
   2087      vim.fs.rm(get_lock_path())
   2088      n.clear()
   2089      eq(2, exec_lua('return #vim.pack.get()'))
   2090      eq(2, vim.tbl_count(get_lock_tbl().plugins))
   2091    end)
   2092  end)
   2093 
   2094  describe('del()', function()
   2095    it('works', function()
   2096      local basic_spec = { src = repos_src.basic, version = 'feat-branch' }
   2097      vim_pack_add({ repos_src.plugindirs, repos_src.defbranch, basic_spec })
   2098 
   2099      local assert_on_disk = function(installed_map)
   2100        local installed = {}
   2101        for p_name, is_installed in pairs(installed_map) do
   2102          eq(is_installed, pack_exists(p_name))
   2103          if is_installed then
   2104            installed[#installed + 1] = p_name
   2105          end
   2106        end
   2107 
   2108        table.sort(installed)
   2109        local locked = vim.tbl_keys(get_lock_tbl().plugins)
   2110        table.sort(locked)
   2111        eq(installed, locked)
   2112      end
   2113 
   2114      assert_on_disk({ basic = true, defbranch = true, plugindirs = true })
   2115 
   2116      -- By default should delete only non-active plugins, even if
   2117      -- there is active one among input plugin names
   2118      n.clear()
   2119      vim_pack_add({ repos_src.defbranch })
   2120      watch_events({ 'PackChangedPre', 'PackChanged' })
   2121 
   2122      local err = pcall_err(exec_lua, function()
   2123        vim.pack.del({ 'basic', 'defbranch', 'plugindirs' })
   2124      end)
   2125      matches('Some plugins are active and were not deleted: defbranch', err)
   2126 
   2127      assert_on_disk({ basic = false, defbranch = true, plugindirs = false })
   2128 
   2129      local msg = "vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'"
   2130      eq(msg, n.exec_capture('messages'))
   2131 
   2132      -- Should trigger relevant events in order as specified in `vim.pack.add()`
   2133      local log = exec_lua('return _G.event_log')
   2134      local find_event = make_find_packchanged(log)
   2135      eq(1, find_event('Pre', 'delete', 'basic', 'feat-branch', false))
   2136      eq(2, find_event('', 'delete', 'basic', 'feat-branch', false))
   2137      eq(3, find_event('Pre', 'delete', 'plugindirs', nil, false))
   2138      eq(4, find_event('', 'delete', 'plugindirs', nil, false))
   2139      eq(4, #log)
   2140 
   2141      -- Should be possible to force delete active plugins
   2142      n.exec('messages clear')
   2143      exec_lua('_G.event_log = {}')
   2144      exec_lua(function()
   2145        vim.pack.del({ 'defbranch' }, { force = true })
   2146      end)
   2147 
   2148      assert_on_disk({ basic = false, defbranch = false, plugindirs = false })
   2149 
   2150      eq("vim.pack: Removed plugin 'defbranch'", n.exec_capture('messages'))
   2151 
   2152      log = exec_lua('return _G.event_log')
   2153      find_event = make_find_packchanged(log)
   2154      eq(1, find_event('Pre', 'delete', 'defbranch', nil, true))
   2155      eq(2, find_event('', 'delete', 'defbranch', nil, false))
   2156      eq(2, #log)
   2157    end)
   2158 
   2159    it('works without prior `add()`', function()
   2160      vim_pack_add({ repos_src.basic })
   2161      n.clear()
   2162 
   2163      eq(true, pack_exists('basic'))
   2164      exec_lua(function()
   2165        vim.pack.del({ 'basic' })
   2166      end)
   2167      eq(false, pack_exists('basic'))
   2168      eq({ plugins = {} }, get_lock_tbl())
   2169    end)
   2170 
   2171    it('works with out of sync lockfile', function()
   2172      vim_pack_add({ repos_src.basic, repos_src.defbranch, repos_src.plugindirs })
   2173      eq(3, vim.tbl_count(get_lock_tbl().plugins))
   2174 
   2175      -- Should first autoinstall missing plugin (with confirmation)
   2176      vim.fs.rm(pack_get_plug_path('basic'), { force = true, recursive = true })
   2177      n.clear()
   2178      mock_confirm(1)
   2179      exec_lua('vim.pack.del({ "defbranch" })')
   2180 
   2181      eq(1, exec_lua('return #_G.confirm_log'))
   2182      eq(true, pack_exists('basic'))
   2183      eq(false, pack_exists('defbranch'))
   2184      eq(true, pack_exists('plugindirs'))
   2185 
   2186      -- Should regenerate absent lockfile (from present plugins)
   2187      vim.fs.rm(get_lock_path())
   2188      n.clear()
   2189      exec_lua('vim.pack.del({ "basic" })')
   2190      eq(1, exec_lua('return #vim.pack.get()'))
   2191      eq({ 'plugindirs' }, vim.tbl_keys(get_lock_tbl().plugins))
   2192    end)
   2193 
   2194    it('validates input', function()
   2195      local function assert(err_pat, input)
   2196        local function del_input()
   2197          vim.pack.del(input)
   2198        end
   2199        matches(err_pat, pcall_err(exec_lua, del_input))
   2200      end
   2201 
   2202      assert('list', nil)
   2203 
   2204      -- Should first check if every plugin name represents installed plugin
   2205      -- If not - stop early before any delete
   2206      vim_pack_add({ repos_src.basic })
   2207 
   2208      assert('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' })
   2209      eq(true, pack_exists('basic'))
   2210 
   2211      -- Empty list is allowed with warning
   2212      n.exec('messages clear')
   2213      exec_lua(function()
   2214        vim.pack.del({})
   2215      end)
   2216      eq('vim.pack: Nothing to remove', n.exec_capture('messages'))
   2217    end)
   2218  end)
   2219 end)