---@diagnostic disable: need-check-nil, undefined-field, param-type-mismatch, assign-type-mismatch -- Drives a live Neovim over RPC and asserts pivi's interactive behavior. -- Run with: just drive -- -- The server is a real Neovim with the real configuration runtime path, so -- keymaps, user commands, autocommands, and buffer state behave as they do -- for a person. Requests are synchronous, so no scenario sleeps. local ADDRESS = '127.0.0.1:' .. tostring( 6800 + math.random( 1, 180 ) ) local server = vim.system( { vim.v.progpath, '--headless', '--noplugin', '-u', 'tests/minimal_init.lua', '--listen', ADDRESS, }, { stdout = false, stderr = false, } ) local channel local connected = vim.wait( 20000, function() local ok, value = pcall( vim.fn.sockconnect, 'tcp', ADDRESS, { rpc = true, } ) if ok and value ~= 0 then channel = value return true end return false end, 100 ) if not connected then io.stderr:write( 'cannot reach the driven neovim at ' .. ADDRESS .. '\n' ) server:kill 'sigterm' os.exit( 1 ) end local remote = function( code, arguments ) return vim.rpcrequest( channel, 'nvim_exec_lua', code, arguments or {} ) end local failures = {} local scenario = function( name, body ) local ok, failure = pcall( body ) if ok then print( 'ok ' .. name ) else failures[#failures + 1] = name print( 'FAIL ' .. name .. ': ' .. tostring( failure ) ) end end local expect = function( condition, message ) if not condition then error( message or 'expectation failed', 2 ) end end remote [[ local command = require 'bugabinga.pivi.command' command.setup() _G.DRIVE = { picked = false } vim.ui.select = function( items, _, on_choice ) _G.DRIVE.picked = true on_choice( nil ) end return true ]] scenario( 'the Pi user command exists with subcommand completion', function() local names = remote [[ return vim.fn.getcompletion('Pi ', 'cmdline') ]] expect( vim.list_contains( names, 'ask' ), 'ask is missing from completion' ) expect( vim.list_contains( names, 'help' ), 'help is missing from completion' ) end ) scenario( 'an unknown subcommand is reported rather than executed', function() local message = remote [[ local seen local original = vim.notify vim.notify = function(text) seen = text end vim.cmd 'Pi levitate' vim.notify = original return seen ]] expect( type( message ) == 'string', 'no message was shown' ) expect( message:find( 'levitate', 1, true ) ~= nil, 'the message does not name the subcommand' ) end ) scenario( 'slash opens command entry only on the first character', function() local result = remote [[ local session = require 'bugabinga.pivi.session' local command = require 'bugabinga.pivi.command' local current = setmetatable( { name = 'drive', buffer = vim.api.nvim_create_buf(false, true) }, { __index = function() return function() end end } ) local bufnr = command.open_prompt(current) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'hello' }) return { first = command.slash_opens_commands(bufnr, 0), later = command.slash_opens_commands(bufnr, 3), filetype = vim.bo[bufnr].filetype, } ]] expect( result.first == true, 'slash does not open at the first character' ) expect( result.later == false, 'slash wrongly opens later in the line' ) expect( result.filetype == 'pivi-input', 'the prompt buffer has the wrong filetype' ) end ) scenario( 'typing slash later in the line still searches', function() local typed = remote [[ vim.cmd 'normal! ggdG' vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'alpha beta' }) vim.api.nvim_win_set_cursor(0, { 1, 4 }) _G.DRIVE.picked = false local mapping for _, entry in ipairs(vim.api.nvim_buf_get_keymap(0, 'n')) do if entry.lhs == '/' then mapping = entry end end return { produced = mapping.callback(), picked = _G.DRIVE.picked } ]] expect( typed.produced == '/', 'slash was swallowed away from the first character' ) expect( typed.picked == false, 'the command picker opened away from the first character' ) end ) scenario( 'status consumers read cached state without any pi process', function() local summary = remote [[ local publish = require 'bugabinga.pivi.publish' local status = require 'bugabinga.pivi.status' publish.reset() local empty = publish.summary() publish.session_status('drive', status.reduce(status.initial(), { type = 'agent_start' })) return { empty = empty, running = publish.summary('drive') } ]] expect( summary.empty == '', 'an unpublished statusline is not empty' ) expect( summary.running == 'pi:run', 'the cached statusline is wrong: ' .. tostring( summary.running ) ) end ) scenario( 'the companion client reaches the running neovim', function() local directory = vim.fn.tempname() vim.fn.mkdir( directory, 'p' ) local request = directory .. '/request.json' local response = directory .. '/response.json' vim.fn.writefile( { vim.json.encode { action = 'help', tag = 'quickfix', }, }, request ) local client = vim.system( { vim.v.progpath, '--headless', '--clean', '-l', 'lua/bugabinga/pivi/extension/client.lua', ADDRESS, request, response, }, { stdout = false, stderr = false, } ):wait( 30000 ) expect( client.code == 0, 'the client exited with ' .. tostring( client.code ) ) local answer = vim.json.decode( table.concat( vim.fn.readfile( response ), '\n' ) ) expect( answer.ok == true, 'the bridge refused: ' .. tostring( answer.error ) ) expect( answer.found == true, 'the help tag did not resolve' ) expect( answer.file == 'quickfix.txt', 'unexpected help file: ' .. tostring( answer.file ) ) end ) scenario( 'the companion client reports an unreachable neovim', function() local directory = vim.fn.tempname() vim.fn.mkdir( directory, 'p' ) local request = directory .. '/request.json' local response = directory .. '/response.json' vim.fn.writefile( { vim.json.encode { action = 'state', }, }, request ) vim.system( { vim.v.progpath, '--headless', '--clean', '-l', 'lua/bugabinga/pivi/extension/client.lua', '127.0.0.1:6', request, response, }, { stdout = false, stderr = false, } ):wait( 30000 ) local answer = vim.json.decode( table.concat( vim.fn.readfile( response ), '\n' ) ) expect( answer.ok == false, 'an unreachable neovim was reported as success' ) end ) pcall( vim.fn.chanclose, channel ) server:kill 'sigterm' if #failures > 0 then print( tostring( #failures ) .. ' scenario(s) failed' ) os.exit( 1 ) end print 'all drive scenarios passed'