repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/tests/bugabinga/pivi/session_spec.lua
Raw---@diagnostic disable: duplicate-set-field, need-check-nil, param-type-mismatch, assign-type-mismatch
local session = require 'bugabinga.pivi.session'
local publish = require 'bugabinga.pivi.publish'
local transcript = require 'bugabinga.pivi.transcript'
local activity = require 'bugabinga.pivi.activity'
local follow = require 'bugabinga.pivi.follow'
local dialog = require 'bugabinga.pivi.dialog'
local entry = require 'bugabinga.pivi.entry'
local render = require 'bugabinga.pivi.render'
local fold = require 'bugabinga.pivi.fold'
local fake = dofile 'tests/helpers/pivi_fake.lua'
local fixture = dofile 'tests/helpers/pivi_fixture.lua'
local create = function( channel, options )
options = options or {}
options.spawn = channel.spawn
options.name = options.name or 'spec'
options.extension = false
options.skill = false
options.executable = options.executable or 'pi'
options.env = {}
return session.create( options )
end
-- Flags that would strip the user's own Pi configuration.
local STRIPPING_FLAGS = {
'--no-extensions',
'--no-skills',
'--no-prompt-templates',
'--no-context-files',
'--no-themes',
'--no-builtin-tools',
'--system-prompt',
}
local commands_of = function( channel, kind )
local found = {}
for _, command in ipairs( channel.commands ) do
if command.type == kind then table.insert( found, command ) end
end
return found
end
describe( 'pivi session', function()
after_each( function()
fake.drain()
session.stop_all()
transcript.forget_all()
publish.reset()
follow.reset()
activity.clear_trail()
fake.close_extra_windows()
vim.cmd 'silent! %bwipeout!'
end )
local temp_buffer = function( lines )
local path = vim.fs.normalize( vim.fn.tempname() .. '.txt' )
vim.fn.writefile( lines, path )
vim.cmd( 'edit! ' .. vim.fn.fnameescape( path ) )
return vim.api.nvim_get_current_buf(), path
end
it( 'starts pi in rpc mode inside the session working directory', function()
local channel = fake.new()
local current = create( channel, { cwd = vim.uv.cwd(), } )
assert.same( { 'pi', '--mode', 'rpc', '--name', 'spec', }, channel.command )
assert.equal( vim.uv.cwd(), channel.options.cwd )
assert.equal( 'spec', current.name )
assert.same( { 'spec', }, session.list() )
end )
it( 'resolves the pi executable to a real path', function()
local built = session.build_command {}
local resolved = built[1]
assert.is_true( resolved:lower():find( 'pi', 1, true ) ~= nil )
-- A bare name would reach the spawner unresolved and fail on Windows.
assert.equal( 1, vim.fn.executable( resolved ) )
end )
it( 'never strips the user\'s own pi configuration', function()
local built = session.build_command { executable = 'pi', }
for _, flag in ipairs( STRIPPING_FLAGS ) do
assert.is_false( vim.list_contains( built, flag ) )
end
assert.is_false( vim.list_contains( built, '--no-tools' ) )
end )
it( 'adds only its companion extension and its skill to the user\'s pi', function()
local plain = session.build_command { extension = false, skill = false, executable = 'pi', }
assert.same( { 'pi', '--mode', 'rpc', }, plain )
local built = session.build_command { executable = 'pi', }
for index = 4, #built, 2 do
local flag = built[index]
assert.is_true( flag == '-e' or flag == '--skill' )
assert.is_true( built[index + 1]:find( 'pivi', 1, true ) ~= nil )
end
end )
it( 'teaches a working session about pivi through its skill', function()
local built = session.build_command {}
local index = vim.fn.index( built, '--skill' ) + 1
assert.is_true( index > 0 )
assert.is_true( built[index + 1]:find( 'SKILL.md', 1, true ) ~= nil )
end )
it( 'sends an idle submission as a plain prompt', function()
local channel = fake.new()
local current = create( channel )
local accepted, rejection = current.submit( 'do the thing', { capture = false, } )
assert.is_true( accepted )
assert.is_nil( rejection )
local prompts = commands_of( channel, 'prompt' )
assert.equal( 1, #prompts )
assert.equal( 'do the thing', prompts[1].message )
end )
it( 'rejects a submission during a run instead of silently dropping it', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'agent_start', }
assert.is_true( channel.settle( function() return current.status.run == 'running' end ) )
local accepted, rejection = current.submit( 'late thought', { capture = false, } )
assert.is_false( accepted )
assert.is_string( rejection )
assert.equal( 0, #commands_of( channel, 'prompt' ) )
end )
it( 'accepts an explicit steering delivery during a run', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'agent_start', }
assert.is_true( channel.settle( function() return current.status.run == 'running' end ) )
local accepted = current.submit( 'focus on errors', { capture = false, delivery = 'steer', } )
assert.is_true( accepted )
local steers = commands_of( channel, 'steer' )
assert.equal( 1, #steers )
assert.equal( 'focus on errors', steers[1].message )
end )
it( 'accepts an explicit follow-up delivery during a run', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'agent_start', }
assert.is_true( channel.settle( function() return current.status.run == 'running' end ) )
assert.is_true( current.submit( 'then summarize', { capture = false, delivery = 'follow_up', } ) )
assert.equal( 1, #commands_of( channel, 'follow_up' ) )
end )
it( 'refuses an unknown delivery', function()
local channel = fake.new()
local current = create( channel )
local accepted, rejection = current.submit( 'x', { capture = false, delivery = 'shout', } )
assert.is_false( accepted )
assert.is_string( rejection )
end )
it( 'streams assistant text into the transcript buffer', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = 'hello ', },
}
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = 'editor', },
}
assert.is_true( channel.settle( function()
return vim.list_contains( transcript.lines( current.buffer ), 'hello editor' )
end ) )
end )
it( 'opens no answer entry for a turn that only calls tools', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
channel.frame { type = 'tool_execution_start', toolCallId = 'c1', toolName = 'read', args = { path = 'greet.lua', }, }
channel.frame { type = 'tool_execution_end', toolCallId = 'c1', toolName = 'read', isError = false, }
channel.flush()
local kinds = {}
for _, item in ipairs( entry.list( current.buffer ) ) do table.insert( kinds, item.kind ) end
assert.same( { 'tool', }, kinds )
assert.is_true( table.concat( transcript.lines( current.buffer ), '\n' ):find( 'read', 1, true ) ~= nil )
end )
it( 'opens exactly one answer entry once real text arrives', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
for _, piece in ipairs { 'all ', 'done', } do
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = piece, },
}
end
channel.flush()
local answers = {}
for _, item in ipairs( entry.list( current.buffer ) ) do
if item.kind == 'answer' then table.insert( answers, item ) end
end
assert.equal( 1, #answers )
assert.is_true( vim.list_contains( transcript.lines( current.buffer ), 'all done' ) )
end )
it( 'drops the blank lines a model opens with', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = '\n\nDone.', },
}
channel.flush()
local lines = transcript.lines( current.buffer )
local answer = entry.list( current.buffer )[1]
assert.equal( 'answer', answer.kind )
assert.equal( 'Done.', lines[answer.first] )
assert.equal( answer.first, answer.last )
end )
it( 'ignores an empty text delta', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = '', },
}
channel.flush()
assert.same( {}, entry.list( current.buffer ) )
end )
it( 'names what a tool acts on, relative to the session directory', function()
local channel = fake.new()
local current = create( channel, { cwd = vim.uv.cwd(), } )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'edit',
args = { path = vim.fs.normalize( vim.uv.cwd() .. '/lua/deep/file.lua' ), },
}
channel.flush()
local rendered = table.concat( transcript.lines( current.buffer ), '\n' )
assert.is_true( rendered:find( 'lua/deep/file.lua', 1, true ) ~= nil )
assert.is_nil( rendered:find( vim.uv.cwd(), 1, true ) )
end )
it( 'marks a failed tool call', function()
local channel = fake.new()
local current = create( channel )
channel.frame { type = 'tool_execution_start', toolCallId = 'c1', toolName = 'read', args = {}, }
channel.frame { type = 'tool_execution_end', toolCallId = 'c1', toolName = 'read', isError = true, }
channel.flush()
assert.is_true( vim.list_contains( transcript.lines( current.buffer ), 'failed' ) )
assert.is_true( entry.list( current.buffer )[1].meta.failed )
end )
it( 'writes a submission exactly as it was typed', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'first\nsecond', { capture = false, } )
local prompt = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
assert.equal( 'prompt', prompt.kind )
assert.equal( 'first\nsecond', table.concat( vim.list_slice( lines, prompt.first, prompt.last ), '\n' ) )
end )
it( 'yields the original message text when an answer is copied', function()
local channel = fake.new()
local current = create( channel )
local authored = '# Heading\n\nSome **bold** text and `code`.\n\n```lua\nlocal x = 1\n```'
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
for piece in authored:gmatch '[^\n]*\n?' do
if piece ~= '' then
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = piece, },
}
end
end
channel.flush()
local answer = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
local copied = table.concat( vim.list_slice( lines, answer.first, answer.last ), '\n' )
assert.equal( 'answer', answer.kind )
assert.equal( authored, copied )
end )
it( 'frames entries with decoration rather than text', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'do it', { capture = false, } )
channel.frame { type = 'tool_execution_start', toolCallId = 'c1', toolName = 'read', args = { path = 'a.lua', }, }
channel.flush()
local rendered = table.concat( transcript.lines( current.buffer ), '\n' )
for _, framing in ipairs { '> ', '\u{25b8} ', '## pi', ' ! ', } do
assert.is_nil( rendered:find( framing, 1, true ) )
end
assert.is_true( #render.marks( current.buffer ) > 0 )
end )
it( 'makes every part of a turn an addressable entry', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'do the thing', { capture = false, } )
channel.frame { type = 'message_start', message = { role = 'assistant', }, }
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'read',
args = { path = 'greet.lua', },
}
channel.frame { type = 'tool_execution_end', toolCallId = 'c1', toolName = 'read', isError = false, }
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'text_delta', contentIndex = 0, delta = 'all done', },
}
channel.frame { type = 'agent_settled', }
channel.flush()
local kinds = {}
for _, item in ipairs( entry.list( current.buffer ) ) do
table.insert( kinds, item.kind )
end
assert.same( { 'prompt', 'tool', 'answer', }, kinds )
local lines = transcript.lines( current.buffer )
for _, item in ipairs( entry.list( current.buffer ) ) do
assert.is_true( item.first >= 1 and item.last <= #lines )
assert.is_true( item.first <= item.last )
end
end )
it( 'keeps a tool entry addressable with its call and result', function()
local channel = fake.new()
local current = create( channel )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'edit',
args = { path = 'greet.lua', },
}
channel.frame {
type = 'tool_execution_end',
toolCallId = 'c1',
toolName = 'edit',
isError = false,
result = { content = { { type = 'text', text = 'edited', }, }, },
}
channel.flush()
local tool = entry.list( current.buffer )[1]
assert.equal( 'tool', tool.kind )
assert.equal( 'edit', tool.meta.tool )
assert.equal( 'greet.lua', tool.meta.args.path )
assert.is_table( tool.meta.result )
assert.is_false( tool.meta.failed )
end )
it( 'shows a tool result collapsed on arrival without changing history', function()
local channel = fake.new()
local current = create( channel )
local win = transcript.show( current.buffer )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'read',
args = { path = 'a.lua', },
}
channel.frame {
type = 'tool_execution_end',
toolCallId = 'c1',
toolName = 'read',
isError = false,
result = { content = { { type = 'text', text = 'line one\nline two\nline three', }, }, },
}
channel.flush()
local tool = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
assert.equal( 'tool', tool.kind )
assert.is_true( tool.last > tool.first )
assert.is_true( vim.list_contains( lines, 'line two' ) )
vim.api.nvim_win_call( win, function()
assert.is_true( fold.closed( tool.first ) )
end )
-- Collapsing is presentation: nothing was submitted, queued, or recorded.
assert.same( { steering = {}, follow_up = {}, }, current.status.queue )
assert.same( {}, current.submissions )
assert.same( {}, commands_of( channel, 'prompt' ) )
end )
it( 'states how a tool ended beside what it acted on', function()
local channel = fake.new()
local current = create( channel )
local win = transcript.show( current.buffer )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'edit',
args = { path = 'greet.lua', },
}
channel.frame {
type = 'tool_execution_end',
toolCallId = 'c1',
toolName = 'edit',
isError = false,
result = {
content = { { type = 'text', text = 'Successfully replaced 1 block(s) in greet.lua.', }, },
details = { diff = ' 1 keep\n-2 old\n+2 new\n', firstChangedLine = 2, },
},
}
channel.flush()
local item = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
-- The outcome is decoration, never buffer text.
assert.equal( 'edit greet.lua', lines[item.first] )
local outcomes = {}
for _, mark in ipairs( render.marks( current.buffer ) ) do
local virt = ( mark[4] or {} ).virt_text
if virt then table.insert( outcomes, virt[1][1] ) end
end
assert.is_true( vim.iter( outcomes ):any( function( text ) return text:find( '+1 -1', 1, true ) ~= nil end ) )
-- And it survives collapsing.
local collapsed = vim.api.nvim_win_call( win, function()
vim.v.foldstart = item.first
vim.v.foldend = item.last
return fold.text()
end )
assert.is_true( collapsed:find( 'greet.lua', 1, true ) ~= nil )
assert.is_true( collapsed:find( '+1 -1', 1, true ) ~= nil )
end )
it( 'shows the output of a running tool as it arrives', function()
local channel = fake.new()
local current = create( channel )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'bash',
args = { command = 'count', },
}
channel.flush()
local opened = entry.list( current.buffer )[1]
assert.equal( opened.first, opened.last )
channel.frame {
type = 'tool_execution_update',
toolCallId = 'c1',
toolName = 'bash',
partialResult = { content = { { type = 'text', text = 'one', }, }, },
}
channel.flush()
local growing = entry.list( current.buffer )[1]
assert.is_true( vim.list_contains( transcript.lines( current.buffer ), 'one' ) )
assert.equal( opened.first, growing.first )
assert.equal( growing.first + 1, growing.last )
channel.frame {
type = 'tool_execution_update',
toolCallId = 'c1',
toolName = 'bash',
partialResult = { content = { { type = 'text', text = 'one\ntwo\nthree', }, }, },
}
channel.flush()
local grown = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
-- The tail was rewritten rather than appended twice.
assert.equal( opened.first, grown.first )
assert.equal( grown.first + 3, grown.last )
assert.same(
{ 'bash count', 'one', 'two', 'three', },
vim.list_slice( lines, grown.first, grown.last )
)
channel.frame {
type = 'tool_execution_end',
toolCallId = 'c1',
toolName = 'bash',
isError = false,
result = { content = { { type = 'text', text = 'one\ntwo\nthree\ndone', }, }, },
}
channel.flush()
local finished = entry.list( current.buffer )[1]
assert.same(
{ 'bash count', 'one', 'two', 'three', 'done', },
vim.list_slice( transcript.lines( current.buffer ), finished.first, finished.last )
)
end )
it( 'keeps later entries intact while a tool streams', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'do it', { capture = false, } )
channel.frame { type = 'tool_execution_start', toolCallId = 'c1', toolName = 'bash', args = { command = 'go', }, }
for _, text in ipairs { 'a', 'a\nb', 'a\nb\nc', } do
channel.frame {
type = 'tool_execution_update',
toolCallId = 'c1',
toolName = 'bash',
partialResult = { content = { { type = 'text', text = text, }, }, },
}
end
channel.flush()
local listed = entry.list( current.buffer )
local prompt = listed[1]
assert.equal( 'prompt', prompt.kind )
assert.equal( 'do it', transcript.lines( current.buffer )[prompt.first] )
assert.is_true( listed[2].first > prompt.last )
end )
it( 'says nothing about a tool that reported nothing', function()
local channel = fake.new()
local current = create( channel )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'write',
args = { path = 'new.lua', },
}
channel.frame {
type = 'tool_execution_end',
toolCallId = 'c1',
toolName = 'write',
isError = false,
result = { content = { { type = 'text', text = 'wrote new.lua', }, }, },
}
channel.flush()
local item = entry.list( current.buffer )[1]
assert.is_nil( require( 'bugabinga.pivi.tool' ).outcome( item.meta ) )
assert.equal( 'write new.lua', transcript.lines( current.buffer )[item.first] )
end )
it( 'runs a banged prompt as a command instead of saying it', function()
local channel = fake.new()
local current = create( channel )
assert.is_true( current.submit( '!just test', { capture = false, } ) )
local ran
for _, sent in ipairs( channel.commands ) do
if sent.type == 'bash' then ran = sent end
assert.is_not.equal( 'prompt', sent.type )
end
assert.is_table( ran )
assert.equal( 'just test', ran.command )
assert.is_false( ran.excludeFromContext )
end )
it( 'keeps a doubly banged command out of the conversation', function()
local channel = fake.new()
local current = create( channel )
current.submit( '!!git status', { capture = false, } )
local ran
for _, sent in ipairs( channel.commands ) do
if sent.type == 'bash' then ran = sent end
end
assert.is_table( ran )
assert.equal( 'git status', ran.command )
assert.is_true( ran.excludeFromContext )
end )
it( 'shows what a command produced inside its own entry', function()
local channel = fake.new()
local current = create( channel )
current.submit( '!echo hi', { capture = false, } )
local ran
for _, sent in ipairs( channel.commands ) do
if sent.type == 'bash' then ran = sent end
end
channel.frame {
id = ran.id,
type = 'response',
command = 'bash',
success = true,
data = { output = 'hi', exitCode = 0, },
}
channel.flush()
local item = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
assert.equal( 'tool', item.kind )
assert.equal( 'bang', item.meta.tool )
assert.is_true( vim.list_contains( lines, 'hi' ) )
assert.is_false( item.meta.failed )
end )
it( 'marks a command that exited badly', function()
local channel = fake.new()
local current = create( channel )
current.submit( '!false', { capture = false, } )
local ran
for _, sent in ipairs( channel.commands ) do
if sent.type == 'bash' then ran = sent end
end
channel.frame {
id = ran.id,
type = 'response',
command = 'bash',
success = true,
data = { output = '', exitCode = 1, },
}
channel.flush()
assert.is_true( entry.list( current.buffer )[1].meta.failed )
end )
it( 'sends a referenced file with what was said', function()
local path = vim.fs.normalize( vim.fn.tempname() .. '.lua' )
vim.fn.writefile( { 'local referenced = true', }, path )
local channel = fake.new()
local current = create( channel )
current.submit( 'explain @' .. path, { capture = false, } )
local sent = commands_of( channel, 'prompt' )[1]
assert.is_table( sent )
assert.is_true( sent.message:find( 'local referenced = true', 1, true ) ~= nil )
assert.is_true( sent.message:find( '<file name=', 1, true ) ~= nil )
-- The transcript still shows what the user actually wrote.
local prompt = entry.list( current.buffer )[1]
local lines = transcript.lines( current.buffer )
assert.equal( 'explain @' .. path, lines[prompt.first] )
end )
it( 'records a rejected submission as its own entry', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'do it', { capture = false, } )
local id = commands_of( channel, 'prompt' )[1].id
channel.frame { id = id, type = 'response', command = 'prompt', success = false, error = 'nope', }
channel.flush()
local listed = entry.list( current.buffer )
local failure = listed[#listed]
assert.equal( 'error', failure.kind )
assert.equal( 'nope', failure.meta.error )
end )
it( 'surfaces a rejected submission in the transcript', function()
local channel = fake.new()
local current = create( channel )
current.submit( 'do it', { capture = false, } )
local id = commands_of( channel, 'prompt' )[1].id
channel.frame { id = id, type = 'response', command = 'prompt', success = false, error = 'nope', }
assert.is_true( channel.settle( function()
for _, line in ipairs( transcript.lines( current.buffer ) ) do
if line:find( 'nope', 1, true ) then return true end
end
return false
end ) )
end )
it( 'publishes cached status through a variable and a user event', function()
local channel = fake.new()
local current = create( channel )
local seen = {}
local group = vim.api.nvim_create_augroup( 'pivi_session_spec', { clear = true, } )
vim.api.nvim_create_autocmd( 'User', {
group = group,
pattern = publish.status_pattern,
callback = function( event ) table.insert( seen, event.data ) end,
} )
channel.frame { type = 'agent_start', }
assert.is_true( channel.settle( function() return #seen > 0 end ) )
assert.equal( 'spec', seen[#seen].session )
assert.equal( 'pi:run', publish.summary 'spec' )
assert.equal( 'running', publish.read().sessions.spec.run )
vim.api.nvim_del_augroup_by_id( group )
end )
it( 'marks the session stopped when the child exits', function()
local channel = fake.new()
local current = create( channel )
channel.close( 0 )
assert.is_true( channel.settle( function() return current.status.connection == 'stopped' end ) )
assert.equal( 'pi:off', require( 'bugabinga.pivi.status' ).summary( current.status ) )
end )
it( 'removes a stopped session from the registry and the published state', function()
local channel = fake.new()
local current = create( channel )
current.stop()
assert.same( {}, session.list() )
assert.is_nil( publish.read().sessions.spec )
assert.is_true( channel.stdin_closed )
end )
it( 'shows streaming edit intent without touching the buffer', function()
local bufnr, path = temp_buffer { 'local value = 1', }
local revision = vim.api.nvim_buf_get_changedtick( bufnr )
local before = vim.api.nvim_buf_get_lines( bufnr, 0, -1, false )
local channel = fake.new()
create( channel )
channel.frame {
type = 'message_update',
assistantMessageEvent = { type = 'toolcall_start', contentIndex = 0, id = 'c1', toolName = 'edit', },
}
channel.frame {
type = 'message_update',
assistantMessageEvent = {
type = 'toolcall_delta',
contentIndex = 0,
delta = '{"path":"' .. path:gsub( '\\', '\\\\' ) .. '","new_text":"local value = 2',
},
}
assert.is_true( channel.settle( function() return #activity.ghost_marks( bufnr ) > 0 end ) )
assert.equal( revision, vim.api.nvim_buf_get_changedtick( bufnr ) )
assert.same( before, vim.api.nvim_buf_get_lines( bufnr, 0, -1, false ) )
channel.frame {
type = 'message_update',
assistantMessageEvent = {
type = 'toolcall_end',
contentIndex = 0,
toolCall = { id = 'c1', name = 'edit', arguments = { path = path, new_text = 'local value = 2', }, },
},
}
assert.is_true( channel.settle( function() return #activity.ghost_marks( bufnr ) == 0 end ) )
assert.same( before, vim.api.nvim_buf_get_lines( bufnr, 0, -1, false ) )
end )
it( 'follows a located tool call when following is enabled', function()
local _, path = temp_buffer { 'one', 'two', 'three', }
local working = vim.api.nvim_get_current_win()
follow.set 'window'
local channel = fake.new()
create( channel )
channel.frame {
type = 'tool_execution_start',
toolCallId = 'c1',
toolName = 'read',
args = { path = path, line = 3, },
}
assert.is_true( channel.settle( function() return follow.window() ~= nil end ) )
assert.equal( working, vim.api.nvim_get_current_win() )
assert.equal( 3, vim.api.nvim_win_get_cursor( follow.window() )[1] )
end )
it( 'answers an extension dialog with a matching identifier', function()
local original = vim.ui.select
vim.ui.select = function( items, _, on_choice ) on_choice( items[1] ) end
local channel = fake.new()
create( channel )
channel.frame {
type = 'extension_ui_request',
id = 'uuid-1',
method = 'select',
title = 'Allow?',
options = { 'Allow', 'Block', },
}
assert.is_true( channel.settle( function()
for _, entry in ipairs( channel.commands ) do
if entry.type == 'extension_ui_response' then return true end
end
return false
end ) )
local answer
for _, entry in ipairs( channel.commands ) do
if entry.type == 'extension_ui_response' then answer = entry end
end
assert.equal( 'uuid-1', answer.id )
assert.equal( 'Allow', answer.value )
vim.ui.select = original
end )
it( 'replays a recorded run made with the user\'s own pi configuration', function()
local channel = fake.new()
local current = create( channel )
local settled = false
current.on( function( event )
if event.type == 'agent_settled' then settled = true end
end )
for _, frame in ipairs( fixture.frames 'rpc-tool-stream.jsonl' ) do
channel.frame( frame )
end
assert.is_true( channel.settle( function() return settled end ) )
assert.equal( 'idle', current.status.run )
assert.same( {}, current.client.failures() )
local rendered = table.concat( transcript.lines( current.buffer ), '\n' )
assert.is_true( rendered:find( 'read', 1, true ) ~= nil )
local answered = false
for _, frame in ipairs( fixture.frames 'rpc-tool-stream.jsonl' ) do
if frame.type == 'message_end' and frame.message and frame.message.role == 'assistant' then
for _, block in ipairs( frame.message.content or {} ) do
-- The transcript drops the blank lines a model opens with.
local text = vim.trim( block.text or '' )
if text ~= '' then
answered = answered or rendered:find( text:sub( 1, 12 ), 1, true ) ~= nil
end
end
end
end
assert.is_true( answered )
end )
it( 'absorbs extension status and widget traffic from a recorded run', function()
dialog.reset()
local channel = fake.new()
local current = create( channel )
local frames = fixture.frames 'rpc-tool-stream.jsonl'
local last_set, requests = 0, 0
for index, frame in ipairs( frames ) do
if frame.type == 'extension_ui_request' then
requests = requests + 1
if frame.statusText or frame.widgetLines then last_set = index end
end
end
assert.is_true( requests > 0 )
assert.is_true( last_set > 0 )
for index = 1, last_set do
channel.frame( frames[index] )
end
channel.flush()
assert.is_true( next( dialog.statuses() ) ~= nil or next( dialog.widgets() ) ~= nil )
for index = last_set + 1, #frames do
channel.frame( frames[index] )
end
channel.flush()
-- The recorded extensions clear their own status and widgets on shutdown.
assert.is_nil( next( dialog.statuses() ) )
assert.is_nil( next( dialog.widgets() ) )
-- Fire and forget requests are never answered.
for _, command in ipairs( channel.commands ) do
assert.is_not.equal( 'extension_ui_response', command.type )
end
assert.same( {}, current.client.failures() )
dialog.reset()
end )
it( 'keeps several sessions with distinct names', function()
local first = create( fake.new(), { name = 'one', } )
local second = create( fake.new(), { name = 'two', } )
assert.same( { 'one', 'two', }, session.list() )
assert.is_not.equal( first.buffer, second.buffer )
session.set_active 'two'
assert.equal( 'two', session.active().name )
end )
end )