return function( directory ) local results = {} local scenario = function( name, body ) local select = vim.ui.select local session_dir = vim.env.PI_CODING_AGENT_SESSION_DIR local ok, failure = xpcall( body, debug.traceback ) vim.ui.select = select vim.env.PI_CODING_AGENT_SESSION_DIR = session_dir table.insert( results, { name = name, ok = ok, error = failure and ( ': ' .. failure ) or nil, } ) return ok end local await = function( predicate, label ) assert( vim.wait( 120000, predicate, 10 ), label or 'predicate deadline' ) end local pivi = require 'bugabinga.pivi' local current local request = function( command, target ) local response ( target or current ).request( command, function( answer ) response = answer end ) await( function() return response ~= nil end, command.type .. ' deadline' ) assert( response.success, response.error ) return response.data end local control = function( name, args ) return request { type = 'prompt', message = '/' .. name .. ( args and ( ' ' .. args ) or '' ), } end local responses = function( steps ) control( 'e2e-responses', vim.json.encode( steps ) ) end local text = function() return table.concat( pivi.transcript.lines( current.buffer ), '\n' ) end local finish = function( command ) local settled = false local unsubscribe = current.on( function( event ) if event.type == 'agent_settled' then settled = true end end ) command() await( function() return settled end, 'agent_settled deadline' ) unsubscribe() request { type = 'get_state', } assert( #current.client.failures() == 0, table.concat( current.client.failures(), '\n' ) ) assert( current.status.run == 'idle' ) end local call_count = function() control 'e2e-count' request { type = 'get_state', } return tonumber( pivi.dialog.statuses()['e2e-count'] ) end local stop = function( live ) live.stop() await( function() return not live.client.is_running() end, 'Pi shutdown deadline' ) end local file = directory .. '/source.lua' vim.fn.writefile( { 'local value = 1', 'return value', }, file ) vim.cmd.edit( file ) local source = vim.api.nvim_get_current_buf() local working = vim.api.nvim_get_current_win() if not scenario( 'start: real Pi, additive user config, faux model, no model call', function() vim.cmd 'Pi start alpha' current = pivi.session.active() assert( current.name == 'alpha' ) local state = request { type = 'get_state', } assert( state.model.provider == 'pivi-e2e', 'refusing metered provider' ) assert( state.sessionId ) local commands = request { type = 'get_commands', } assert( vim.iter( commands.commands ):any( function( item ) return item.sourceInfo and item.sourceInfo.scope == 'user' end ), 'configured user resources not loaded' ) assert( call_count() == 0 ) assert( vim.api.nvim_win_get_buf( working ) == source ) end ) then return results end scenario( 'ask: real streamed markdown, authored text, extmark decoration, cached status', function() local authored = '# Answer\n\n**native** `code`\n\n```lua\nreturn 42\n```' responses { { content = authored, }, } finish( function() vim.cmd 'Pi ask e2e question' end ) assert( text():find( authored, 1, true ) ) assert( not text():find( '## pi', 1, true ) ) assert( #pivi.render.marks( current.buffer ) > 0 ) assert( pivi.summary() == 'pi:idle' ) local before = call_count() for _ = 1, 100 do pivi.summary() end assert( before == call_count() ) end ) scenario( 'prompt: native mapping submits, hides without stopping, draft survives', function() vim.cmd 'Pi prompt' local prompt = vim.api.nvim_get_current_buf() vim.api.nvim_buf_set_lines( prompt, 0, -1, false, { 'from prompt', } ) responses { { content = 'prompt answer', }, } finish( function() local maps = vim.api.nvim_buf_get_keymap( prompt, 'n' ) for _, map in ipairs( maps ) do if map.lhs == '(pivi-submit)' then map.callback(); return end end error 'missing submit mapping' end ) assert( text():find( 'prompt answer', 1, true ) ) assert( vim.api.nvim_buf_get_lines( prompt, 0, -1, false )[1] == '' ) vim.api.nvim_buf_set_lines( prompt, 0, -1, false, { 'saved draft', } ) vim.api.nvim_win_close( vim.fn.bufwinid( prompt ), true ) assert( current.client.is_running() ) vim.cmd 'Pi prompt' assert( vim.api.nvim_get_current_buf() == prompt ) assert( vim.api.nvim_get_current_line() == 'saved draft' ) end ) scenario( 'slash: discoverable command dialogs, generic status/widgets, no model call', function() local before = call_count() local original = vim.ui.select vim.ui.select = function( items, _, callback ) callback( items[2] ) end vim.cmd 'Pi command e2e-dialog' await( function() return pivi.dialog.statuses()['e2e-choice'] == 'second' and pivi.dialog.widgets()['e2e-widget'] ~= nil end ) vim.ui.select = original assert( pivi.dialog.widgets()['e2e-widget'] ) assert( call_count() == before ) vim.cmd 'Pi command model' request { type = 'get_state', } assert( call_count() == before, 'terminal command became a model prompt' ) end ) scenario( 'queue: prompt accept steers, follow-up listed/cleared, abort settles', function() responses { { content = 'held answer', hold = true, }, } vim.cmd 'Pi ask hold the run' await( function() return current.status.run == 'running' end ) vim.cmd 'Pi prompt' local prompt = vim.api.nvim_get_current_buf() vim.api.nvim_buf_set_lines( prompt, 0, -1, false, { 'steering message', } ) for _, map in ipairs( vim.api.nvim_buf_get_keymap( prompt, 'n' ) ) do if map.lhs == '(pivi-submit)' then map.callback() end end vim.cmd 'Pi follow-up follow-up message' await( function() return #current.status.queue.steering == 1 and #current.status.queue.follow_up == 1 end ) vim.cmd 'Pi queue' vim.cmd 'Pi clear' await( function() return #current.status.queue.steering == 0 and #current.status.queue.follow_up == 0 end ) vim.cmd 'Pi abort' await( function() return current.status.run == 'idle' end ) assert( request( { type = 'get_state', } ).isStreaming == false ) vim.api.nvim_buf_set_lines( prompt, 0, -1, false, { 'saved draft', } ) end ) scenario( 'delivery: steering/follow-up both reach persisted conversation after release', function() responses { { content = 'initial answer', hold = true, }, { content = 'steered answer', }, { content = 'follow-up answer', }, } local settled = false local unsubscribe = current.on( function( event ) if event.type == 'agent_settled' then settled = true end end ) vim.cmd 'Pi ask delivery barrier' await( function() return current.status.run == 'running' end ) vim.cmd 'Pi steer delivered steering' vim.cmd 'Pi follow-up delivered follow-up' await( function() return #current.status.queue.steering == 1 and #current.status.queue.follow_up == 1 end ) control 'e2e-release' await( function() return settled end ) unsubscribe() local history = vim.json.encode( request { type = 'get_messages', } ) assert( history:find( 'delivered steering', 1, true ) ) assert( history:find( 'delivered follow-up', 1, true ) ) assert( text():find( 'follow-up answer', 1, true ) ) end ) scenario( 'help: real independent faux run while working, live docs, history/queue/status untouched', function() responses { { content = 'held working answer', hold = true, }, } vim.cmd 'Pi ask independent help barrier' await( function() return current.status.run == 'running' end ) local history_before = #request( { type = 'get_entries', } ).entries local queue_before = vim.deepcopy( current.status.queue ) local revision = vim.api.nvim_buf_get_changedtick( source ) vim.cmd 'Pi help what is quickfix?' local helpbuf = pivi.transcript.ensure( 'help' ) await( function() return table.concat( pivi.transcript.lines( helpbuf ), '\n' ):find( 'Live quickfix documentation fetched.', 1, true ) ~= nil end ) assert( current.status.run == 'running' ) assert( #request( { type = 'get_entries', } ).entries == history_before ) assert( vim.deep_equal( queue_before, current.status.queue ) ) assert( vim.api.nvim_buf_get_changedtick( source ) == revision ) assert( not text():find( 'what is quickfix?', 1, true ) ) vim.cmd 'Pi abort' await( function() return current.status.run == 'idle' end ) end ) scenario( 'help safety: real malicious faux Lua/write calls cannot change editor/files', function() local revision = vim.api.nvim_buf_get_changedtick( source ) local original = vim.fn.readfile( file ) local run = pivi.help.ask( 'attempt mutation', { env = vim.tbl_extend( 'force', pivi.runtime.bridge_environment(), { PIVI_E2E_ATTACK = 'vim.api.nvim_buf_set_lines(' .. source .. ', 0, -1, false, {"mutated"})', PIVI_E2E_ATTACK_FILE = file, } ), } ) await( function() return run.done end ) await( function() return not run.client.is_running() end ) assert( run.answer == 'Mutation attempts finished.' ) assert( vim.api.nvim_buf_get_changedtick( source ) == revision, 'help Lua modified buffer' ) assert( vim.deep_equal( original, vim.fn.readfile( file ) ), 'help wrote file' ) end ) scenario( 'tools: unsaved reads authoritative, editor state/Lua cross-process bridge', function() vim.api.nvim_buf_set_lines( source, 0, 1, false, { 'local value = 7', } ) vim.api.nvim_buf_call( source, function() vim.cmd 'let &undolevels = &undolevels' end ) responses { { content = { { type = 'toolCall', id = 'read-live', name = 'read', arguments = { path = file, }, }, }, stopReason = 'toolUse', }, { content = { { type = 'toolCall', id = 'lua-live', name = 'nvim_lua', arguments = { code = 'return vim.api.nvim_buf_get_lines(' .. source .. ', 0, -1, false)', }, }, }, stopReason = 'toolUse', }, { content = 'tools finished', }, } finish( function() vim.cmd 'Pi ask inspect live editor' end ) assert( text():find( 'local value = 7', 1, true ) ) local tools = vim.tbl_filter( function( item ) return item.kind == 'tool' end, pivi.entry.list( current.buffer ) ) assert( #tools >= 2 ) for _, item in ipairs( tools ) do assert( not item.meta.failed, vim.inspect( item.meta ) ) end end ) scenario( 'edit: real Pi tool, inert streaming ghost, local mutation, disk coherence, trail, one undo', function() local revision = vim.api.nvim_buf_get_changedtick( source ) local ghost = false local ghost_error local unsubscribe = current.on( function( event ) if event.type == 'message_update' and #pivi.activity.ghost_marks( source ) > 0 then ghost = true if vim.api.nvim_buf_get_changedtick( source ) ~= revision then ghost_error = 'streaming arguments mutated buffer' end end end ) responses { { content = { { type = 'toolCall', id = 'edit-live', name = 'edit', arguments = { path = file, oldText = 'local value = 7', newText = 'local value = 8', }, }, }, stopReason = 'toolUse', }, { content = 'edit finished', }, } finish( function() vim.cmd 'Pi ask edit live buffer' end ) unsubscribe() assert( ghost, 'real streamed arguments never showed ghost text' ) assert( not ghost_error, ghost_error ) assert( vim.api.nvim_buf_get_lines( source, 0, 1, false )[1] == 'local value = 8' ) assert( vim.fn.readfile( file )[1] == 'local value = 8' ) assert( #pivi.activity.trail( source ) > 0 ) local item = pivi.entry.list( current.buffer ) local edited for _, candidate in ipairs( item ) do if candidate.meta.call == 'edit-live' then edited = candidate end end assert( edited and edited.meta.result.details.diff ) local win = vim.fn.bufwinid( current.buffer ) vim.api.nvim_win_set_cursor( win, { edited.first, 0, } ) local recorded = #request( { type = 'get_entries', } ).entries vim.api.nvim_win_call( win, function() assert( pivi.fold.closed( edited.first ) ) vim.cmd 'normal! zo' assert( not pivi.fold.closed( edited.first ) ) vim.cmd 'normal! zc' vim.cmd 'Pi diff' end ) assert( vim.api.nvim_win_get_buf( working ) == source ) assert( #request( { type = 'get_entries', } ).entries == recorded ) pivi.comparison.close() vim.api.nvim_buf_call( source, function() vim.cmd 'undo' end ) assert( vim.api.nvim_buf_get_lines( source, 0, 1, false )[1] == 'local value = 7' ) end ) scenario( 'findings/follow/failure: real tools, native qf, dedicated viewport, red outcome', function() vim.api.nvim_set_current_win( working ) vim.cmd 'Pi follow window' responses { { content = { { type = 'toolCall', id = 'findings-live', name = 'nvim_findings', arguments = { title = 'e2e findings', items = { { path = file, line = 2, text = 'finding', }, }, }, }, }, stopReason = 'toolUse', }, { content = { { type = 'toolCall', id = 'follow-live', name = 'read', arguments = { path = file, }, }, }, stopReason = 'toolUse', }, { content = { { type = 'toolCall', id = 'failed-live', name = 'read', arguments = { path = directory .. '/absent.txt', }, }, }, stopReason = 'toolUse', }, { content = 'findings finished', }, } finish( function() vim.cmd 'Pi ask report findings' end ) assert( vim.fn.getqflist()[1].text == 'finding' ) assert( pivi.follow.window() and pivi.follow.window() ~= working ) assert( vim.api.nvim_get_current_win() == working ) local failed for _, item in ipairs( pivi.entry.list( current.buffer ) ) do if item.meta.call == 'failed-live' then failed = item end end assert( failed and failed.meta.failed and failed.first == failed.last, vim.inspect( failed ) ) vim.cmd 'Pi follow' end ) scenario( 'native entry motions/detail/location: real restored-capable tool surfaces preserve source', function() local edited for _, item in ipairs( pivi.entry.list( current.buffer ) ) do if item.meta.call == 'edit-live' then edited = item end end local win = pivi.transcript.show( current.buffer ) vim.api.nvim_set_current_win( win ) vim.api.nvim_win_set_cursor( win, { edited.first, 0, } ) local invoke = function( intent ) for _, map in ipairs( vim.api.nvim_buf_get_keymap( current.buffer, 'n' ) ) do if map.lhs == intent then map.callback(); return end end error( 'missing intent ' .. intent ) end invoke '(pivi-next-tool)' assert( vim.api.nvim_win_get_cursor( win )[1] > edited.first ) invoke '(pivi-previous-tool)' assert( vim.api.nvim_win_get_cursor( win )[1] == edited.first ) invoke '(pivi-inspect-entry)' local detail for _, candidate in ipairs( vim.api.nvim_list_wins() ) do if vim.api.nvim_win_get_config( candidate ).relative ~= '' then detail = candidate end end assert( detail and vim.api.nvim_buf_get_name( vim.api.nvim_win_get_buf( detail ) ):find( '/detail', 1, true ) ) vim.api.nvim_win_close( detail, true ) invoke '(pivi-goto-location)' assert( pivi.follow.window() ~= working ) assert( vim.api.nvim_win_get_cursor( pivi.follow.window() )[1] == edited.meta.result.details.firstChangedLine ) assert( vim.api.nvim_win_get_buf( working ) == source ) pivi.follow.reset() end ) scenario( 'selection: unsaved queued source/revision retained through mutation and delivery', function() vim.api.nvim_set_current_win( working ) vim.api.nvim_buf_set_lines( source, 0, 1, false, { 'local selected = "unsaved original"', } ) responses { { content = 'selection held', hold = true, }, { content = 'selection delivered', }, } vim.cmd '1,1Pi ask selection snapshot' await( function() return current.status.run == 'running' end ) local shot = current.submissions[#current.submissions].snapshot assert( shot.selected_text == 'local selected = "unsaved original"' and shot.modified ) local revision = shot.revision current.submit( 'queued selection', { delivery = 'follow_up', source = { bufnr = source, win = working, range = { kind = 'range', first = { 1, 0, }, last = { 1, 0, }, }, }, } ) await( function() return #current.status.queue.follow_up == 1 end ) local queued = current.submissions[#current.submissions].snapshot vim.api.nvim_buf_set_lines( source, 0, 1, false, { 'local selected = "later mutation"', } ) assert( queued.revision == revision and queued.selected_text == shot.selected_text ) control 'e2e-release' await( function() return current.status.run == 'idle' end ) local persisted = vim.json.encode( request { type = 'get_messages', } ) assert( persisted:find( 'unsaved original', 1, true ) and not persisted:find( 'later mutation', 1, true ) ) vim.api.nvim_buf_set_lines( source, 0, -1, false, { 'local value = 7', 'return value', } ) end ) scenario( 'loclist/diagnostics/plugin docs: real companion tools preserve unrelated namespaces', function() local foreign = vim.api.nvim_create_namespace( 'e2e-foreign-diagnostics' ) vim.diagnostic.set( foreign, source, { { lnum = 0, col = 0, message = 'foreign', }, } ) local tags = vim.fn.getcompletion( 'plenary', 'help' ) assert( #tags > 0, 'installed plenary docs unavailable' ) responses { { content = { { type = 'toolCall', id = 'native-lists', name = 'nvim_lua', arguments = { code = 'local a = require("bugabinga.pivi.activity"); a.window_findings(' .. working .. ', {{buffer=' .. source .. ',line=2,text="local finding"}}); a.diagnose(' .. source .. ', {{line=2,text="agent finding"}}); return true', }, }, }, stopReason = 'toolUse', }, { content = { { type = 'toolCall', id = 'plugin-doc', name = 'nvim_help', arguments = { tag = tags[1], }, }, }, stopReason = 'toolUse', }, { content = 'native lists and plugin docs finished', }, } finish( function() vim.cmd 'Pi ask native lists and plugin documentation' end ) assert( vim.fn.getloclist( working )[1].text == 'local finding' ) assert( pivi.activity.diagnostics( source )[1].source == 'pi' ) assert( vim.diagnostic.get( source, { namespace = foreign, } )[1].message == 'foreign' ) vim.api.nvim_win_call( working, function() vim.cmd 'lfirst' end ) assert( vim.api.nvim_win_get_cursor( working )[1] == 2 ) local docs for _, item in ipairs( pivi.entry.list( current.buffer ) ) do if item.meta.call == 'plugin-doc' then docs = item end end assert( docs and not docs.meta.failed and pivi.tool.produced( docs.meta ):find( 'plenary', 1, true ) ) vim.diagnostic.reset( foreign, source ) pivi.activity.clear_diagnostics( source ) end ) scenario( 'help payload: originating visible region/layout/selection/messages/error bounded on real wire', function() vim.api.nvim_set_current_win( working ) local lines = { 'visible-origin-sentinel', } for index = 2, 600 do lines[index] = 'hidden-line-' .. index end lines[600] = 'hidden-tail-sentinel' vim.api.nvim_buf_set_lines( source, 0, -1, false, lines ) vim.api.nvim_win_set_cursor( working, { 1, 0, } ) vim.api.nvim_win_call( working, function() vim.cmd 'normal! zt' end ) vim.fn.setpos( "'<", { source, 1, 1, 0, } ) vim.fn.setpos( "'>", { source, 1, 5, 0, } ) vim.api.nvim_echo( { { 'recent-message-sentinel', }, }, true, {} ) vim.v.errmsg = 'last-error-sentinel' local run = pivi.help.ask( 'bounded live help' ) local message = run.client.sent[1].message assert( message:find( 'visible-origin-sentinel', 1, true ) ) assert( not message:find( 'hidden-tail-sentinel', 1, true ) ) assert( message:find( 'selection', 1, true ) and message:find( 'windows:', 1, true ) ) assert( message:find( 'recent-message-sentinel', 1, true ) and message:find( 'last-error-sentinel', 1, true ) ) assert( #message < 20000 ) await( function() return run.done and not run.client.is_running() end ) vim.v.errmsg = '' vim.api.nvim_buf_set_mark( source, '<', 0, 0, {} ) vim.api.nvim_buf_set_mark( source, '>', 0, 0, {} ) vim.api.nvim_buf_set_lines( source, 0, -1, false, { 'local value = 7', 'return value', } ) end ) scenario( 'search: real upstream grep grouped decoration, authored output, native quickfix', function() local second = directory .. '/second.lua' vim.fn.writefile( { 'local value = 2', 'return value', }, second ) responses { { content = { { type = 'toolCall', id = 'grep-live', name = 'grep', arguments = { pattern = 'value', path = directory, glob = '*.lua', literal = true, }, }, }, stopReason = 'toolUse', }, { content = 'search finished', }, } finish( function() vim.cmd 'Pi ask search values' end ) local searched for _, item in ipairs( pivi.entry.list( current.buffer ) ) do if item.meta.call == 'grep-live' then searched = item end end assert( searched and not searched.meta.failed ) assert( #pivi.tool.matches( searched.meta ) >= 4 ) assert( #vim.fn.getqflist() >= 4 ) local headings = 0 for _, mark in ipairs( pivi.render.marks( current.buffer ) ) do if mark[2] >= searched.first and mark[2] < searched.last and mark[4].virt_lines then headings = headings + 1 end end assert( headings >= 2, 'missing file groups' ) vim.api.nvim_win_call( working, function() vim.cmd 'cfirst' end ) assert( vim.api.nvim_win_get_cursor( working )[1] >= 1 ) vim.api.nvim_win_set_buf( working, source ) end ) scenario( 'bash: native RPC progressive deltas, truncation/full-output navigation without model', function() local release = directory .. '/shell-release' local script = directory .. '/shell.cjs' vim.fn.writefile( { 'const fs = require("node:fs"); console.log("shell-first-sentinel");', 'function poll() { if (!fs.existsSync(process.argv[2])) return setImmediate(poll);', 'for (let i = 0; i < 3000; i++) console.log("line-" + i + " " + "x".repeat(80)); } poll();', }, script ) local ended, answer = false, nil assert( current.bash( 'node "' .. vim.fs.normalize( script ) .. '" "' .. vim.fs.normalize( release ) .. '"', true, function( response ) ended, answer = true, response end ) ) local streamed = vim.wait( 20000, function() return ended or text():find( 'shell-first-sentinel', 1, true ) ~= nil end, 10 ) vim.fn.writefile( { 'release', }, release ) assert( streamed and not ended, vim.inspect( answer or current.client.failures() ) ) vim.fn.writefile( { 'release', }, release ) await( function() return ended end ) local items = pivi.entry.list( current.buffer ) local shell = items[#items] assert( not shell.meta.failed, vim.inspect( shell.meta ) ) assert( pivi.tool.truncated( shell.meta ) ) local full = pivi.tool.full_output( shell.meta ) assert( full and vim.fn.filereadable( full ) == 1 ) assert( #vim.fn.readfile( full ) >= 3001 ) local win = pivi.transcript.show( current.buffer ) vim.api.nvim_set_current_win( win ) vim.api.nvim_win_set_cursor( win, { shell.first, 0, } ) pivi.navigate.locate( current.buffer ) assert( vim.fs.normalize( vim.api.nvim_buf_get_name( vim.api.nvim_win_get_buf( pivi.follow.window() ) ) ) == vim.fs.normalize( full ) ) pivi.follow.reset() vim.fn.delete( full ) end ) scenario( 'model/thinking/name/stats: native RPC commands without global persistence', function() vim.cmd 'Pi model pivi-e2e/faux-1' vim.cmd 'Pi thinking off' vim.cmd 'Pi name persisted task' vim.cmd 'Pi session' await( function() local data = request { type = 'get_state', } return data.sessionName == 'persisted task' end ) assert( request( { type = 'get_session_stats', } ).cost == 0 ) end ) local identity, history, old_client scenario( 'restart: real child replaced, identity/history/place/draft retained, prompt rebound', function() identity = request( { type = 'get_state', } ).sessionId history = request( { type = 'get_entries', } ).entries old_client = current.client local transcript = current.buffer local win = vim.fn.bufwinid( transcript ) vim.api.nvim_win_set_cursor( win, { 2, 0, } ) vim.cmd 'Pi restart' await( function() pivi = require 'bugabinga.pivi' local resumed = pivi.session.active() if resumed and resumed ~= current and resumed.restored then current = resumed; return true end return false end, 'restart restoration deadline' ) assert( not old_client.is_running(), 'old Pi child remains alive' ) assert( current.buffer == transcript ) assert( request( { type = 'get_state', } ).sessionId == identity ) assert( #request( { type = 'get_entries', } ).entries == #history ) assert( vim.api.nvim_win_get_cursor( win )[1] == 2 ) assert( call_count() == 0, 'restart called model' ) local tool_entries = vim.tbl_filter( function( item ) return item.kind == 'tool' end, pivi.entry.list( current.buffer ) ) assert( #tool_entries >= 3, 'tool history missing' ) assert( vim.iter( tool_entries ):any( function( item ) return item.meta.call == 'edit-live' and item.meta.result.details and item.meta.result.details.diff ~= nil end ), 'recorded edit details lost during restoration' ) vim.cmd 'Pi prompt' local prompt = vim.api.nvim_get_current_buf() assert( vim.api.nvim_get_current_line() == 'saved draft' ) responses { { content = 'rebound answer', }, } vim.api.nvim_buf_set_lines( prompt, 0, -1, false, { 'after restart', } ) finish( function() for _, map in ipairs( vim.api.nvim_buf_get_keymap( prompt, 'n' ) ) do if map.lhs == '(pivi-submit)' then map.callback(); return end end end ) assert( text():find( 'rebound answer', 1, true ) ) end ) scenario( 'sessions: create/switch active, source buffer independent, stop explicit', function() vim.cmd 'Pi start beta' local beta = pivi.session.active() request( { type = 'get_state', }, beta ) assert( beta.name == 'beta' ) local original = vim.ui.select vim.ui.select = function( items, _, callback ) callback( 'alpha' ) end vim.cmd 'Pi sessions' vim.ui.select = original assert( pivi.session.active().name == 'alpha' ) local alpha_id = request( { type = 'get_state', } ).sessionId local focused = vim.api.nvim_get_current_win() vim.cmd 'Pi restart' await( function() pivi = require 'bugabinga.pivi' local alpha, other = pivi.session.get( 'alpha' ), pivi.session.get( 'beta' ) if alpha and other and alpha.restored and other.restored then current, beta = alpha, other return true end return false end ) assert( pivi.session.active().name == 'alpha' ) assert( vim.api.nvim_get_current_win() == focused ) assert( request( { type = 'get_state', } ).sessionId == alpha_id ) stop( beta ) assert( current.client.is_running() ) end ) scenario( 'resume latest: persistent identity/history, no new conversation/model call', function() identity = request( { type = 'get_state', } ).sessionId stop( current ) vim.cmd 'Pi resume' current = pivi.session.active() await( function() return current.restored end ) assert( request( { type = 'get_state', } ).sessionId == identity ) assert( text():find( 'rebound answer', 1, true ) ) assert( call_count() == 0 ) end ) scenario( 'resume identity: editor completion reads real Pi store, history no model call', function() stop( current ) local stored = pivi.store.sessions( directory ) assert( #stored > 0 ) assert( vim.list_contains( vim.fn.getcompletion( 'Pi resume ', 'cmdline' ), identity ) ) vim.cmd( 'Pi resume ' .. identity ) current = pivi.session.active() await( function() return current.restored end ) assert( request( { type = 'get_state', } ).sessionId == identity ) assert( call_count() == 0 ) end ) scenario( 'prior picker: choose real persisted identity, no model call', function() stop( current ) local original = vim.ui.select local picked vim.ui.select = function( items, options, callback ) assert( options.prompt == 'pi prior session' ) for _, item in ipairs( items ) do if item.id == identity then picked = item; callback( item ); return end end error 'saved identity absent from picker' end local ok, failure = pcall( vim.cmd, 'Pi prior' ) vim.ui.select = original assert( ok, failure ) assert( picked ) current = pivi.session.active() await( function() return current.restored end ) assert( request( { type = 'get_state', } ).sessionId == identity and call_count() == 0 ) end ) scenario( 'descriptor missing identity: quiet no restoration, no fallback child', function() local before = #_G.PIVI_E2E_CHILDREN assert( #pivi.restore( { { name = 'absent', session = 'missing-id', cwd = directory, }, } ) == 0 ) assert( #pivi.restore( nil ) == 0 and #_G.PIVI_E2E_CHILDREN == before ) assert( pivi.session.get( 'absent' ) == nil ) end ) scenario( 'descriptor: idempotent native-session consumer restore', function() local descriptor = pivi.descriptor( directory ) assert( #descriptor == 1 and descriptor[1].session == identity ) assert( #pivi.restore( descriptor ) == 0 ) stop( current ) assert( #pivi.restore( descriptor ) == 1 ) current = pivi.session.active() await( function() return current.restored end ) assert( request( { type = 'get_state', } ).sessionId == identity ) end ) scenario( 'branched restore: recorded native tree selects active ancestry and tool details only', function() local state = request { type = 'get_state', } local records = vim.fn.readfile( state.sessionFile ) local header = vim.json.decode( records[1] ) header.id = 'e2e-branched-history' local output = { vim.json.encode( header ), } local originals = {} for index = 2, #records do originals[#originals + 1] = vim.json.decode( records[index] ) end local previous = vim.NIL local append = function( original ) local record = vim.deepcopy( original ) record.id = string.format( '%08x', #output ) record.parentId = previous previous = record.id output[#output + 1] = vim.json.encode( record ) end local naming for _, record in ipairs( originals ) do if record.type == 'session_info' and record.name then naming = record end end assert( naming, 'native recorded session name required' ) append( naming ) local shared = previous for _, original in ipairs( originals ) do local record = vim.deepcopy( original ) if record.type == 'message' then if record.message.role == 'user' then record.message.content = 'DISCARDED_BRANCH_MARKER' end if record.message.role == 'toolResult' then record.message.content = { { type = 'text', text = 'DISCARDED_TOOL_MARKER', }, } record.message.details = { diff = 'DISCARDED_DIFF_MARKER', } end end append( record ) end previous = shared for _, record in ipairs( originals ) do append( record ) end local path = directory .. '/branched.jsonl' vim.fn.writefile( output, path ) local live = pivi.session.create { name = 'branched', resume = path, } await( function() return live.restored end ) local data = request( { type = 'get_entries', }, live ) assert( #data.entries == #output - 1 and data.leafId == previous ) local expected = require( 'bugabinga.pivi.backfill' ).plan( data ) local actual = pivi.entry.list( live.buffer ) assert( #actual == #expected and #actual > 0 ) local rendered = table.concat( vim.api.nvim_buf_get_lines( live.buffer, 0, -1, false ), '\n' ) assert( not rendered:find( 'DISCARDED_', 1, true ), 'discarded branch rendered' ) local paired = false for _, item in ipairs( actual ) do if item.kind == 'tool' and item.meta.call == 'edit-live' then assert( item.meta.result.details.diff and item.meta.result.details.diff ~= 'DISCARDED_DIFF_MARKER' ) paired = true end end assert( paired, 'selected native tool details missing' ) request( { type = 'prompt', message = '/e2e-count', }, live ) request( { type = 'get_state', }, live ) local counted for _, event in ipairs( live.client.events ) do if event.type == 'extension_ui_request' and event.statusKey == 'e2e-count' then counted = event.statusText end end assert( tonumber( counted ) == 0, 'branched restoration called model' ) stop( live ) pivi.session.set_active( current.name ) end ) scenario( 'large restore: 2000 real message copies, main-loop yields, addressable history, no model', function() local state = request { type = 'get_state', } local records = vim.fn.readfile( state.sessionFile ) local header = vim.json.decode( records[1] ) header.id = 'e2e-large-history' local prompt, answer, model, naming for _, line in ipairs( records ) do local record = vim.json.decode( line ) if record.type == 'session_info' and record.name then naming = record end if record.type == 'model_change' then model = record end if record.type == 'message' and record.message.role == 'user' then prompt = record end if record.type == 'message' and record.message.role == 'assistant' and record.message.stopReason == 'stop' then answer = record end end assert( prompt and answer ) local output = { vim.json.encode( header ), } local previous = vim.NIL local append = function( original ) local record = vim.deepcopy( original ) record.id = string.format( '%08x', #output ) record.parentId = previous previous = record.id table.insert( output, vim.json.encode( record ) ) end assert( naming, 'recorded session name required to avoid user auto-naming model calls' ) append( naming ) if model then append( model ) end for _ = 1, 1000 do append( prompt ); append( answer ) end local path = directory .. '/large.jsonl' vim.fn.writefile( output, path ) local live, timer local ok, failure = xpcall( function() live = pivi.session.create { name = 'large', resume = path, } local yields = 0 local rejected_new = false timer = assert( vim.uv.new_timer() ) timer:start( 1, 1, vim.schedule_wrap( function() if #pivi.entry.list( live.buffer ) > 0 and not live.restored then yields = yields + 1 if not rejected_new then pivi.session.set_active( live.name ) local before = #live.client.sent vim.cmd 'Pi new' assert( #live.client.sent == before, 'new_session sent during yielded history restoration' ) rejected_new = true end end end ) ) local started = vim.uv.hrtime() local completed = vim.wait( 120000, function() return live.restored end, 10 ) timer:stop() timer:close() assert( completed, 'large history restoration deadline' ) local elapsed = ( vim.uv.hrtime() - started ) / 1e6 assert( yields > 1, 'restoration blocked main loop' ) assert( rejected_new, 'new during yielded restoration never exercised' ) assert( #pivi.entry.list( live.buffer ) == 2000 ) local restored = request( { type = 'get_entries', }, live ).entries assert( #restored >= #output - 1 ) assert( #vim.tbl_filter( function( item ) return item.type == 'message' and ( item.message.role == 'user' or item.message.role == 'assistant' ) end, restored ) == 2000 ) request( { type = 'prompt', message = '/e2e-count', }, live ) request( { type = 'get_state', }, live ) local counted for _, event in ipairs( live.client.events ) do if event.type == 'extension_ui_request' and event.statusKey == 'e2e-count' then counted = event.statusText end end assert( tonumber( counted ) == 0, 'restoration model calls: ' .. tostring( counted ) ) assert( elapsed < 10000, 'large restoration exceeded 10s budget: ' .. elapsed .. 'ms' ) local saved = pivi.descriptor( directory ) local descriptor for _, item in ipairs( saved ) do if item.name == 'large' then descriptor = item end end assert( descriptor and vim.fs.normalize( descriptor.file ) == vim.fs.normalize( path ), vim.inspect( descriptor ) ) vim.env.PI_CODING_AGENT_SESSION_DIR = nil vim.cmd 'Pi restart' await( function() pivi = require 'bugabinga.pivi' local resumed, other = pivi.session.get( 'large' ), pivi.session.get( current.name ) if resumed and other and resumed.restored and other.restored then live, current = resumed, other return true end return false end, 'arbitrary file restart deadline' ) assert( request( { type = 'get_state', }, live ).sessionId == header.id ) assert( #request( { type = 'get_entries', }, live ).entries == #restored ) assert( #pivi.entry.list( live.buffer ) == 2000 ) end, debug.traceback ) if timer and not timer:is_closing() then timer:stop(); timer:close() end pivi = require 'bugabinga.pivi' live = pivi.session.get( 'large' ) or live local cleaned, cleanup_error = pcall( function() if live then stop( live ) end end ) current = pivi.session.get( current.name ) or current pivi.session.set_active( current.name ) if vim.api.nvim_win_is_valid( working ) then vim.api.nvim_set_current_win( working ) end assert( cleaned, cleanup_error ) assert( ok, failure ) end ) scenario( 'new/export: fresh history in same process, HTML export temp only', function() local client = current.client vim.cmd 'Pi new' await( function() return #request( { type = 'get_messages', } ).messages == 0 end ) assert( current.client == client ) responses { { content = 'fresh answer', }, } finish( function() vim.cmd 'Pi ask fresh question' end ) assert( not text():find( 'rebound answer', 1, true ) ) local output = directory .. '/export.html' vim.cmd( 'Pi export ' .. output ) await( function() return vim.fn.filereadable( output ) == 1 end ) end ) scenario( 'stop: owned Pi child exits, published state removed', function() if vim.env.PIVI_E2E_INJECT_HANDLER_FAILURE == '1' then local handled = false current.request( { type = 'get_state', }, function() handled = true error 'injected owned RPC handler failure' end ) await( function() return handled end ) request { type = 'get_state', } end vim.cmd 'Pi stop' await( function() return not current.client.is_running() end ) assert( #pivi.session.list() == 0 ) assert( pivi.summary() == '' ) end ) return results end