Luigit
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 )