import { afterEach, describe, expect, it, vi } from 'vitest'
import { PassThrough } from 'node:stream'
import { LocalPtySession } from '@deepseek-ai/dsh-terminal-bash/src/session.ts'
import type { ResolvedConfig } from '@deepseek-ai/dsh-terminal-bash/src/config.ts'
import type { TerminalSendOperation, TerminalSessionStatus, TerminalSignal } from '@deepseek-ai/dsh-terminal'
import type {
  SubprocessOutcome,
  SubprocessTerminalHandle,
  SubprocessTerminalSignal,
} from '@deepseek-ai/dsh-subprocess'
import { TerminalError } from '@deepseek-ai/dsh-terminal'
import type {
  ProcessIdentity,
  ProcessInspector,
} from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'

class FakeInspector implements ProcessInspector {
  pgid: number | undefined = 456
  waiting = false
  members: ProcessIdentity[] = []
  alive = new Set<number>()
  groups: Array<[number, TerminalSignal]> = []
  processes: Array<[number, 'SIGTERM' | 'SIGKILL']> = []
  throwGroup = false
  throwProcess = false
  removeOnSignal = true

  foregroundPgid() { return this.pgid }
  isStdinWaiting() { return this.waiting }
  processTree() { return this.members }
  processSession() { return [] }
  isAlive(identity: ProcessIdentity) { return this.alive.has(identity.pid) }
  signalGroup(pgid: number, signal: TerminalSignal) {
    if (this.throwGroup) throw new Error('group failed')
    this.groups.push([pgid, signal])
  }
  signalProcess(identity: ProcessIdentity, signal: 'SIGTERM' | 'SIGKILL') {
    if (this.throwProcess) throw new Error('process raced')
    this.processes.push([identity.pid, signal])
    if (this.removeOnSignal) this.alive.delete(identity.pid)
  }
}

class FakeTerminal implements SubprocessTerminalHandle {
  pid = 123
  readonly output = new PassThrough()
  readonly writes: string[] = []
  readonly kills: string[] = []
  readonly outcome = Promise.withResolvers<SubprocessOutcome>()
  readonly done = this.outcome.promise
  throwWrite = false
  throwKill = false
  autoExitOnKill = true
  terminateError: Error | undefined
  private cleanup: Promise<void> | undefined

  constructor(public inspector = new FakeInspector()) {}

  emitData(data: string): void {
    this.output.write(Buffer.from(data, 'utf8'))
  }

  emitBytes(data: Uint8Array): void {
    this.output.write(data)
  }

  emitError(error: Error): void {
    this.output.emit('error', error)
  }

  emitFailure(error: unknown): void {
    this.output.end()
    this.outcome.reject(error)
  }

  emitExit(exitCode = 0, signal?: number): void {
    this.output.end()
    this.outcome.resolve({
      exitCode: signal === undefined || signal === 0 ? exitCode : null,
      signal: signal === 9 ? 'SIGKILL' : signal === 15 ? 'SIGTERM' : null,
    })
  }

  async write(data: string): Promise<void> {
    if (this.throwWrite) throw new Error('write failed')
    this.writes.push(data)
  }

  async inspectForeground() {
    const processGroupId = this.inspector.foregroundPgid()
    return processGroupId === undefined
      ? undefined
      : { processGroupId, inputWaiting: this.inspector.isStdinWaiting() }
  }

  async signalForeground(signal: SubprocessTerminalSignal): Promise<number> {
    const foreground = await this.inspectForeground()
    if (foreground === undefined) throw new Error(`cannot resolve foreground process group for terminal ${this.pid}`)
    if (signal === 'SIGKILL' && foreground.processGroupId === this.pid) {
      throw new Error('refusing to SIGKILL the terminal shell; terminate the terminal session instead')
    }
    this.inspector.signalGroup(foreground.processGroupId, signal)
    return foreground.processGroupId
  }

  terminate(): Promise<void> {
    if (this.cleanup !== undefined) return this.cleanup
    const cleanup = this.terminateOnce()
    this.cleanup = cleanup
    void cleanup.catch(() => { this.cleanup = undefined })
    return cleanup
  }

  private async terminateOnce(): Promise<void> {
    if (this.terminateError !== undefined) throw this.terminateError
    if (this.throwKill) throw new Error('kill failed')
    this.kills.push('SIGTERM')
    if (this.autoExitOnKill) this.emitExit(0, 15)
  }
}

function makeSession(
  terminal: FakeTerminal,
  inspector: FakeInspector,
  resolved: ResolvedConfig,
): LocalPtySession {
  terminal.inspector = inspector
  return new LocalPtySession(terminal, resolved)
}

function config(overrides: Partial<ResolvedConfig> = {}): ResolvedConfig {
  return {
    backendType: 'shell', shellPath: '/bin/bash', shellArgs: [], rows: 24, cols: 80,
    scrollbackLines: 10, scrollbackMaxBytes: 128, maxReadBytes: 64,
    pollIntervalMs: 10, exactProbeAfterMs: 20, idleSilenceMs: 50, handoffGraceMs: 10, timeoutMs: 100,
    disposeGraceMs: 20,
    ...overrides,
  }
}

afterEach(() => { vi.useRealTimers() })

async function initialize(session: LocalPtySession, terminal: FakeTerminal): Promise<void> {
  const pending = session.initialize()
  terminal.emitData('\x1b]133;D;0\x07dsh> ')
  await vi.advanceTimersByTimeAsync(10)
  await pending
}

describe('LocalPtySession readiness and output', () => {
  it('lets queued terminal output run before the first post-write readiness poll', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const inspect = terminal.inspectForeground.bind(terminal)
    let inspections = 0
    terminal.inspectForeground = async () => {
      inspections += 1
      return await inspect()
    }
    const operation = session.startSend({ text: 'true', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(inspections).toBe(1)

    await vi.advanceTimersByTimeAsync(0)
    expect(inspections).toBe(1)
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('discards prompt readiness observed during asynchronous pre-write inspection', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    await initialize(session, terminal)

    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    terminal.inspectForeground = async () => await inspection.promise
    const operation = session.startSend({ text: 'long-running-command', submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })

    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    inspection.resolve({ processGroupId: 456, inputWaiting: true })
    await vi.advanceTimersByTimeAsync(20)
    expect(terminal.writes).toEqual(['long-running-command\r'])
    expect(settled).toBe(false)

    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('captures prompt MOTD, writes submit explicitly, and settles exact stdin waits', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)
    expect(session.motd).toBe('dsh> ')

    inspector.waiting = true
    const operation = session.startSend({ text: 'python3', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(terminal.writes).toEqual(['python3\r'])
    inspector.pgid = 789
    terminal.emitData('Python\r\n>>> ')
    await vi.advanceTimersByTimeAsync(20)
    expect(await operation.done).toMatchObject({ waitReason: 'stdin_read', viewport: 'Python\n>>> ', sessionStatus: { kind: 'running' } })
    expect(operation.cancel()).toBe(false)
  })

  it('does not reuse a pre-write stdin wait as post-write readiness', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    inspector.waiting = true
    const operation = session.startSend({ text: 'echo ready', submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })
    await vi.advanceTimersByTimeAsync(20)
    expect(settled).toBe(false)

    inspector.waiting = false
    await vi.advanceTimersByTimeAsync(10)
    expect(settled).toBe(false)
    inspector.waiting = true
    await vi.advanceTimersByTimeAsync(10)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('tracks a pre-write wait exit before exact probing begins', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config({
      exactProbeAfterMs: 50,
      idleSilenceMs: 100,
      timeoutMs: 200,
    }))
    await initialize(session, terminal)

    inspector.waiting = true
    const operation = session.startSend({ text: 'fast command', submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })
    inspector.waiting = false
    await vi.advanceTimersByTimeAsync(10)
    inspector.waiting = true
    await vi.advanceTimersByTimeAsync(30)
    expect(settled).toBe(false)
    await vi.advanceTimersByTimeAsync(10)
    expect(settled).toBe(true)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('distinguishes inferred idle, timeout, exit signal, and operation reads', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)
    inspector.pgid = undefined

    const inferred = session.startSend({ text: 'sleep', submit: false })
    terminal.emitData('working')
    expect(inferred.readOutput()).toEqual({ delta: 'working', truncated: false })
    await vi.advanceTimersByTimeAsync(60)
    expect((await inferred.done).waitReason).toBe('inferred_idle')

    const timeout = session.startSend({ text: 'blocked', submit: false })
    await vi.advanceTimersByTimeAsync(40)
    terminal.emitData('.')
    await vi.advanceTimersByTimeAsync(40)
    terminal.emitData('.')
    await vi.advanceTimersByTimeAsync(30)
    expect((await timeout.done).waitReason).toBe('timeout')

    const exiting = session.startSend({ text: 'exit', submit: true })
    terminal.emitExit(7, 9)
    expect(await exiting.done).toMatchObject({ waitReason: 'session_exit', sessionStatus: { kind: 'exited', exitCode: null, signal: 'SIGKILL' } })
    expect(() => session.startSend({ text: '', submit: false })).toThrow('has exited')
  })

  it('cancels with foreground-group SIGINT, observes AbortSignal, and contains write failures', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const controller = new AbortController()
    const operation = session.startSend({ text: 'sleep', submit: true, signal: controller.signal })
    expect(() => session.startSend({ text: 'again', submit: true })).toThrow('active send')
    controller.abort()
    await Promise.resolve()
    await Promise.resolve()
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    expect(terminal.writes).not.toContain('\x03')
    terminal.emitData('\x1b]133;D;130\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await operation.done

    const aborted = new AbortController()
    aborted.abort()
    expect(() => session.startSend({ text: '', submit: false, signal: aborted.signal })).toThrow('aborted before write')

    terminal.throwWrite = true
    const failed = session.startSend({ text: 'x', submit: false })
    await expect(failed.done).rejects.toThrow('write failed')
    const failedInternal = failed as unknown as { append(text: string): void; fail(error: unknown): void }
    failedInternal.append('ignored')
    failedInternal.fail(new Error('ignored'))
  })

  it('does not write a send canceled during asynchronous foreground inspection', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    terminal.inspectForeground = async () => await inspection.promise
    const controller = new AbortController()
    const operation = session.startSend({ text: 'must not execute', submit: true, signal: controller.signal })
    controller.abort()
    inspection.resolve({ processGroupId: 456, inputWaiting: false })
    await Promise.resolve()
    await Promise.resolve()

    expect(terminal.writes).toEqual([])
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    terminal.emitData('\x1b]133;D;130\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await operation.done
  })

  it('retains a canceled send when the pre-write inspection rejects while its signal is in flight', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const failed = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    terminal.inspectForeground = async () => await failed.promise
    const uncanceled = session.startSend({ text: 'plain failure', submit: true })
    failed.reject(new Error('inspect failed before write'))
    await expect(uncanceled.done).rejects.toThrow('inspect failed before write')

    terminal.inspectForeground = FakeTerminal.prototype.inspectForeground.bind(terminal)
    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    terminal.inspectForeground = async () => await inspection.promise
    const signalGate = Promise.withResolvers<undefined>()
    terminal.signalForeground = async (signal) => {
      await signalGate.promise
      inspector.signalGroup(456, signal)
      return 456
    }
    const controller = new AbortController()
    const operation = session.startSend({ text: 'must stay owned', submit: true, signal: controller.signal })
    controller.abort()
    inspection.reject(new Error('transient inspection failure'))
    await Promise.resolve()
    await Promise.resolve()

    // The slot stays reserved while the cancellation's foreground signal is in flight.
    expect(() => session.startSend({ text: 'successor', submit: true })).toThrow('active send')
    terminal.inspectForeground = async () => ({ processGroupId: 456, inputWaiting: false })
    signalGate.resolve(undefined)
    await vi.advanceTimersByTimeAsync(0)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])

    terminal.emitData('\x1b]133;D;130\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await operation.done
  })

  it('retains a canceled send until asynchronous foreground signalling settles', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const signalGate = Promise.withResolvers<undefined>()
    terminal.signalForeground = async (signal) => {
      await signalGate.promise
      const foreground = await terminal.inspectForeground()
      if (foreground === undefined) throw new Error('cannot resolve foreground')
      inspector.signalGroup(foreground.processGroupId, signal)
      return foreground.processGroupId
    }
    const operation = session.startSend({ text: 'first', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)

    terminal.emitData('\x1b]133;D;130\x07dsh> ')
    await vi.advanceTimersByTimeAsync(100)
    expect((await operation.done).waitReason).toBe('timeout')
    expect(() => session.startSend({ text: 'successor', submit: true })).toThrow('active send')
    signalGate.resolve(undefined)
    await vi.advanceTimersByTimeAsync(0)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    expect(inspector.groups).not.toContainEqual([789, 'SIGINT'])
  })

  it('does not let an in-flight readiness inspection release a canceled send before signalling settles', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const readiness = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    let inspections = 0
    terminal.inspectForeground = async () => {
      inspections += 1
      if (inspections === 1) return { processGroupId: 456, inputWaiting: false }
      if (inspections === 2) return await readiness.promise
      return { processGroupId: 456, inputWaiting: true }
    }
    const signalling = Promise.withResolvers<undefined>()
    const signalled = Promise.withResolvers<number>()
    terminal.signalForeground = async (signal) => {
      await signalling.promise
      const foreground = await terminal.inspectForeground()
      if (foreground === undefined) throw new Error('cannot resolve foreground')
      inspector.signalGroup(foreground.processGroupId, signal)
      signalled.resolve(foreground.processGroupId)
      return foreground.processGroupId
    }

    const operation = session.startSend({ text: 'first', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    await vi.advanceTimersByTimeAsync(10)
    expect(inspections).toBe(2)
    await vi.advanceTimersByTimeAsync(50)
    expect(operation.cancel()).toBe(true)
    let settled = false
    void operation.done.then(() => { settled = true })

    readiness.resolve({ processGroupId: 456, inputWaiting: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(settled).toBe(false)
    expect(() => session.startSend({ text: 'successor', submit: true })).toThrow(TerminalError)

    signalling.resolve(undefined)
    expect(await signalled.promise).toBe(456)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    await session.close('test complete')
    expect((await operation.done).waitReason).toBe('session_exit')
  })

  it('does not let an in-flight readiness failure release a canceled send before signalling settles', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const readiness = Promise.withResolvers<never>()
    let inspections = 0
    terminal.inspectForeground = async () => {
      inspections += 1
      if (inspections === 1) return { processGroupId: 456, inputWaiting: false }
      if (inspections === 2) return await readiness.promise
      return { processGroupId: 456, inputWaiting: true }
    }
    const signalling = Promise.withResolvers<undefined>()
    const signalled = Promise.withResolvers<number>()
    terminal.signalForeground = async (signal) => {
      await signalling.promise
      const foreground = await terminal.inspectForeground()
      if (foreground === undefined) throw new Error('cannot resolve foreground')
      inspector.signalGroup(foreground.processGroupId, signal)
      signalled.resolve(foreground.processGroupId)
      return foreground.processGroupId
    }

    const operation = session.startSend({ text: 'first', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    await vi.advanceTimersByTimeAsync(10)
    expect(inspections).toBe(2)
    expect(operation.cancel()).toBe(true)
    let settled = false
    void operation.done.then(() => { settled = true })

    readiness.reject(new Error('inspection failed during cancellation'))
    await Promise.resolve()
    await Promise.resolve()
    expect(settled).toBe(false)
    expect(() => session.startSend({ text: 'successor', submit: true })).toThrow(TerminalError)

    signalling.resolve(undefined)
    expect(await signalled.promise).toBe(456)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    await session.close('test complete')
    expect((await operation.done).waitReason).toBe('session_exit')
  })

  it('signals only after an in-flight provider write lands', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const writeGate = Promise.withResolvers<undefined>()
    terminal.write = async () => { await writeGate.promise }
    const operation = session.startSend({ text: 'must be interrupted', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)
    await vi.advanceTimersByTimeAsync(20)
    expect(inspector.groups).toEqual([])

    writeGate.resolve(undefined)
    await vi.advanceTimersByTimeAsync(0)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])
    terminal.emitData('\x1b]133;D;130\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await operation.done
  })

  it('does not signal when a cancelled provider write rejects', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const writeGate = Promise.withResolvers<undefined>()
    terminal.write = async () => { await writeGate.promise }
    const operation = session.startSend({ text: 'rejected write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)

    const rejected = expect(operation.done).rejects.toThrow('write failed after cancellation')
    writeGate.reject(new Error('write failed after cancellation'))
    await rejected
    expect(inspector.groups).toEqual([])

    const next = session.startSend({ text: '', submit: false })
    await vi.advanceTimersByTimeAsync(100)
    expect((await next.done).waitReason).toBe('inferred_idle')
  })

  it('releases a timed-out cancellation after the provider write and signal settle', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const writeGate = Promise.withResolvers<undefined>()
    terminal.write = async () => { await writeGate.promise }
    const operation = session.startSend({ text: 'slow cancelled write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)
    await vi.advanceTimersByTimeAsync(100)

    expect((await operation.done).waitReason).toBe('timeout')
    expect(() => session.startSend({ text: 'must wait', submit: true })).toThrow(expect.objectContaining({
      code: 'SEND_ACTIVE',
    }))

    writeGate.resolve(undefined)
    await vi.advanceTimersByTimeAsync(0)
    expect(inspector.groups).toContainEqual([456, 'SIGINT'])

    const next = session.startSend({ text: '', submit: false })
    await vi.advanceTimersByTimeAsync(100)
    expect((await next.done).waitReason).toBe('inferred_idle')
  })

  it('retains the absolute timeout after cancellation while output stays active', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const operation = session.startSend({ text: 'ignore-sigint-and-write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)
    for (let elapsed = 20; elapsed <= 100; elapsed += 20) {
      terminal.emitData('.')
      await vi.advanceTimersByTimeAsync(20)
    }

    expect(await operation.done).toMatchObject({ waitReason: 'timeout' })
  })

  it('does not resume cancellation polling after the terminal exits during signalling', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const signalGate = Promise.withResolvers<undefined>()
    terminal.signalForeground = async () => {
      await signalGate.promise
      return 456
    }
    const operation = session.startSend({ text: 'first', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)
    terminal.emitExit(0)
    await expect(operation.done).resolves.toMatchObject({ waitReason: 'session_exit' })

    signalGate.resolve(undefined)
    await vi.advanceTimersByTimeAsync(10)
    expect(session.status()).toEqual({ kind: 'exited', exitCode: 0, signal: null })
  })

  it('retains send ownership after timeout until an asynchronous provider write settles', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const writeGate = Promise.withResolvers<undefined>()
    terminal.write = async () => { await writeGate.promise }
    const operation = session.startSend({ text: 'slow write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    await vi.advanceTimersByTimeAsync(100)

    expect((await operation.done).waitReason).toBe('timeout')
    expect(() => session.startSend({ text: 'must wait', submit: true })).toThrow('active send')

    writeGate.resolve(undefined)
    await Promise.resolve()
    await Promise.resolve()
    const rejectedWrite = Promise.withResolvers<undefined>()
    terminal.write = async () => { await rejectedWrite.promise }
    const rejected = session.startSend({ text: 'late rejection', submit: true })
    await vi.advanceTimersByTimeAsync(100)
    expect((await rejected.done).waitReason).toBe('timeout')
    rejectedWrite.reject(new Error('write failed after timeout'))
    await Promise.resolve()
    await Promise.resolve()

    const next = session.startSend({ text: '', submit: false })
    await vi.advanceTimersByTimeAsync(100)
    expect((await next.done).waitReason).toBe('inferred_idle')
  })

  it('retains send ownership when cancellation signalling fails during an asynchronous write', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const writeGate = Promise.withResolvers<undefined>()
    terminal.write = async () => { await writeGate.promise }
    let signalCalls = 0
    terminal.signalForeground = async () => {
      signalCalls += 1
      throw new Error('interrupt failed')
    }
    const operation = session.startSend({ text: 'slow write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(operation.cancel()).toBe(true)
    await vi.advanceTimersByTimeAsync(20)
    expect(signalCalls).toBe(0)
    expect(() => session.startSend({ text: 'must wait', submit: true })).toThrow('active send')

    writeGate.resolve(undefined)
    await expect(operation.done).rejects.toThrow('interrupt failed')
    expect(signalCalls).toBe(1)
    expect(session.status()).toEqual({ kind: 'exited', exitCode: null, signal: null })
    expect(() => session.startSend({ text: '', submit: false })).toThrow('has exited')
  })

  it('handles startup exit, unknown exit signals, cancel-write failure, and stale polls', async () => {
    vi.useFakeTimers()
    const startupTerminal = new FakeTerminal()
    const startup = new LocalPtySession(startupTerminal, config())
    const initializing = startup.initialize(new AbortController().signal)
    startupTerminal.emitExit(1)
    await expect(initializing).rejects.toThrow('exited during startup')
    expect(startup.status()).toEqual({ kind: 'exited', exitCode: 1, signal: null })

    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    await initialize(session, terminal)
    const operation = session.startSend({ text: '', submit: false })
    const operationInternal = operation as unknown as {
      append(text: string): void
      settle(reason: 'timeout', status: TerminalSessionStatus, inherited: boolean): void
    }
    operationInternal.append('')
    const sessionInternal = session as unknown as {
      pollReadiness(operation: TerminalSendOperation): void
      interrupt(operation: TerminalSendOperation): void
      schedulePoll(operation: TerminalSendOperation): void
      polling: boolean
      statusValue: TerminalSessionStatus
      appendOutput(text: string): void
    }
    sessionInternal.appendOutput('')
    sessionInternal.pollReadiness({} as TerminalSendOperation)
    sessionInternal.schedulePoll({} as TerminalSendOperation)
    sessionInternal.polling = true
    sessionInternal.schedulePoll(operation)
    sessionInternal.polling = false
    sessionInternal.interrupt({} as TerminalSendOperation)
    sessionInternal.statusValue = { kind: 'exited', exitCode: 2, signal: null }
    sessionInternal.pollReadiness(operation)
    await operation.done
    operationInternal.settle('timeout', { kind: 'running' }, false)

    const unknownTerminal = new FakeTerminal()
    const unknown = new LocalPtySession(unknownTerminal, config())
    unknownTerminal.emitExit(1, 999)
    await vi.waitFor(() => {
      expect(unknown.status()).toEqual({ kind: 'exited', exitCode: null, signal: null })
    })

    const cancelTerminal = new FakeTerminal()
    const cancelInspector = new FakeInspector()
    const cancel = makeSession(cancelTerminal, cancelInspector, config())
    await initialize(cancel, cancelTerminal)
    const cancellable = cancel.startSend({ text: '', submit: false })
    cancelInspector.throwGroup = true
    expect(cancellable.cancel()).toBe(true)
    await expect(cancellable.done).rejects.toThrow('group failed')
    expect(cancellable.cancel()).toBe(false)

    const missingGroupTerminal = new FakeTerminal()
    const missingGroupInspector = new FakeInspector()
    const missingGroup = makeSession(missingGroupTerminal, missingGroupInspector, config())
    await initialize(missingGroup, missingGroupTerminal)
    missingGroupInspector.pgid = undefined
    const unresolved = missingGroup.startSend({ text: '', submit: false })
    expect(unresolved.cancel()).toBe(true)
    await expect(unresolved.done).rejects.toThrow('cannot resolve foreground process group')
  })

  it('does not treat zero-output startup silence as readiness and fails on startup timeout', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    let settled = false
    const initializing = session.initialize().then(() => { settled = true })
    await vi.advanceTimersByTimeAsync(60)
    expect(settled).toBe(false)
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await initializing

    const timeoutTerminal = new FakeTerminal()
    const timeout = new LocalPtySession(timeoutTerminal, config())
    const timedOut = expect(timeout.initialize()).rejects.toThrow('startup timeout')
    await vi.advanceTimersByTimeAsync(100)
    await timedOut
  })

  it('preserves the caller abort reason when startup cannot resolve a foreground group', async () => {
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    inspector.pgid = undefined
    const session = makeSession(terminal, inspector, config())
    const controller = new AbortController()
    const reason = new Error('startup cancelled')

    const initializing = session.initialize(controller.signal)
    const rejected = expect(initializing).rejects.toBe(reason)
    controller.abort(reason)

    await rejected
  })

  it('waits for printable prompt text when the startup marker is split from PS1', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    let settled = false
    const initializing = session.initialize().then(() => { settled = true })

    terminal.emitData('\x1b]133;D;0\x07')
    await vi.advanceTimersByTimeAsync(20)
    expect(settled).toBe(false)

    terminal.emitData('dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    await initializing
    expect(session.motd).toBe('dsh> ')
  })

  it('does not attribute a delayed prior prompt to the current send', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config({ idleSilenceMs: 100, timeoutMs: 200 }))
    await initialize(session, terminal)

    const operation = session.startSend({ text: "printf 'PID=%s\\n' \"$!\"", submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })
    await Promise.resolve()
    await Promise.resolve()

    terminal.emitData('\x1b]133;D;0\x07dsh> printf \'PID=%s\\n\' "$!"\r\n')
    await vi.advanceTimersByTimeAsync(20)
    expect(settled).toBe(false)

    terminal.emitData('PID=123\r\n\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    expect(await operation.done).toMatchObject({ waitReason: 'stdin_read' })
  })

  it('retains a prompt marker until the startup shell regains the foreground group', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const operation = session.startSend({ text: 'run', submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })
    await Promise.resolve()
    await Promise.resolve()
    inspector.pgid = 789
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(50)
    expect(settled).toBe(false)

    inspector.pgid = 456
    await vi.advanceTimersByTimeAsync(10)
    expect(settled).toBe(true)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('holds the idle fallback for the configured handoff grace, not one poll', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config({ handoffGraceMs: 40 }))
    await initialize(session, terminal)

    const operation = session.startSend({ text: 'run', submit: true })
    let settled = false
    void operation.done.then(() => { settled = true })
    await Promise.resolve()
    await Promise.resolve()
    inspector.pgid = 789
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    // One poll past the silence bound would already have settled inferred_idle.
    await vi.advanceTimersByTimeAsync(70)
    expect(settled).toBe(false)

    inspector.pgid = 456
    await vi.advanceTimersByTimeAsync(10)
    expect((await operation.done).waitReason).toBe('stdin_read')
  })

  it('falls back to inferred idle when a foreground child emits an inherited prompt marker', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const operation = session.startSend({ text: 'bash -i', submit: true })
    inspector.pgid = 789
    terminal.emitData('\x1b]133;D;0\x07child> ')
    await vi.advanceTimersByTimeAsync(100)

    expect((await operation.done).waitReason).toBe('inferred_idle')
  })

  it('contains terminal transport failures and preserves the first failure', async () => {
    const terminal = new FakeTerminal()
    terminal.terminateError = new Error('cleanup after transport failure')
    const session = new LocalPtySession(terminal, config())
    const operation = session.startSend({ text: '', submit: false })
    terminal.output.emit('data', 'plain text')
    terminal.emitError(new Error('output transport failed'))
    ;(session as unknown as { onTransportFailure(error: unknown): void })
      .onTransportFailure(new Error('later failure'))
    await expect(operation.done).rejects.toThrow('output transport failed')
    expect(session.status()).toEqual({ kind: 'exited', exitCode: null, signal: null })
    terminal.terminateError = undefined
    await expect(session.close('transport')).rejects.toThrow('output transport failed')

    const rejectedTerminal = new FakeTerminal()
    const rejected = new LocalPtySession(rejectedTerminal, config())
    const rejectedOperation = rejected.startSend({ text: '', submit: false })
    rejectedTerminal.emitFailure('raw transport failure')
    await expect(rejectedOperation.done).rejects.toThrow('raw transport failure')
  })

  it('replaces invalid UTF-8 terminal output', async () => {
    const chunkTerminal = new FakeTerminal()
    const chunkSession = new LocalPtySession(chunkTerminal, config())
    const chunkOperation = chunkSession.startSend({ text: '', submit: false })
    chunkTerminal.emitBytes(Uint8Array.from([0xff]))
    expect(chunkOperation.readOutput()).toEqual({ delta: '�', truncated: false })
    chunkTerminal.emitExit()
    await chunkOperation.done

    const endTerminal = new FakeTerminal()
    const endSession = new LocalPtySession(endTerminal, config())
    const endOperation = endSession.startSend({ text: '', submit: false })
    endTerminal.emitBytes(Uint8Array.from([0xe2]))
    endTerminal.emitExit()
    expect((await endOperation.done).viewport).toBe('�')
  })

  it('contains readiness inspection failure and a stale inspection result', async () => {
    vi.useFakeTimers()
    const failedTerminal = new FakeTerminal()
    const failedSession = new LocalPtySession(failedTerminal, config())
    const failedOperation = failedSession.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    failedTerminal.inspectForeground = async () => { throw new Error('inspect failed') }
    const failedInternal = failedSession as unknown as {
      pollReadiness(operation: TerminalSendOperation): Promise<void>
    }
    await failedInternal.pollReadiness(failedOperation)
    await expect(failedOperation.done).rejects.toThrow('inspect failed')

    const staleTerminal = new FakeTerminal()
    const staleSession = new LocalPtySession(staleTerminal, config())
    const staleOperation = staleSession.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    const gate = Promise.withResolvers<ReturnType<FakeTerminal['inspectForeground']> extends Promise<infer T> ? T : never>()
    staleTerminal.inspectForeground = async () => await gate.promise
    const staleInternal = staleSession as unknown as {
      active: TerminalSendOperation | undefined
      pollReadiness(operation: TerminalSendOperation): Promise<void>
    }
    const polling = staleInternal.pollReadiness(staleOperation)
    staleInternal.active = undefined
    gate.resolve({ processGroupId: 456, inputWaiting: false })
    await polling
    ;(staleOperation as unknown as {
      settle(reason: 'timeout', status: TerminalSessionStatus, inherited: boolean): void
    }).settle('timeout', { kind: 'running' }, false)
  })

  it('reschedules readiness for a new send after a stale remote inspection releases the poll slot', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const old = session.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    let block = true
    terminal.inspectForeground = async () => block
      ? await inspection.promise
      : { processGroupId: 456, inputWaiting: false }
    const internals = session as unknown as {
      pollReadiness(operation: TerminalSendOperation): Promise<void>
      settleActive(reason: 'timeout'): void
    }
    const stalePoll = internals.pollReadiness(old)
    internals.settleActive('timeout')
    await old.done

    block = false
    const current = session.startSend({ text: '', submit: false })
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await Promise.resolve()
    await Promise.resolve()
    inspection.resolve({ processGroupId: 456, inputWaiting: false })
    await stalePoll
    await vi.advanceTimersByTimeAsync(10)

    expect((await current.done).waitReason).toBe('stdin_read')
  })

  it('does not poll a successor before its own pre-write inspection and write complete', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    await initialize(session, terminal)

    const old = session.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    const staleInspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    const successorInspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    let inspectCalls = 0
    terminal.inspectForeground = async () => {
      inspectCalls += 1
      return inspectCalls === 1 ? await staleInspection.promise : await successorInspection.promise
    }
    const internals = session as unknown as {
      pollReadiness(operation: TerminalSendOperation): Promise<void>
      settleActive(reason: 'timeout'): void
    }
    const stalePoll = internals.pollReadiness(old)
    internals.settleActive('timeout')
    await old.done

    const current = session.startSend({ text: 'successor', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    staleInspection.resolve({ processGroupId: 456, inputWaiting: false })
    await stalePoll
    await vi.advanceTimersByTimeAsync(10)
    expect(inspectCalls).toBe(2)
    expect(terminal.writes).toEqual([])

    successorInspection.resolve({ processGroupId: 456, inputWaiting: false })
    await Promise.resolve()
    await Promise.resolve()
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    expect(terminal.writes).toEqual(['successor\r'])
    expect((await current.done).waitReason).toBe('stdin_read')
  })

  it('contains stale timer, write, inspection, and interrupt continuations', async () => {
    vi.useFakeTimers()
    const settle = (operation: TerminalSendOperation): void => {
      ;(operation as unknown as {
        settle(reason: 'timeout', status: TerminalSessionStatus, inherited: boolean): void
      }).settle('timeout', { kind: 'running' }, false)
    }

    const deadlineTerminal = new FakeTerminal()
    const deadlineSession = new LocalPtySession(deadlineTerminal, config())
    const deadlineOperation = deadlineSession.startSend({ text: '', submit: false })
    ;(deadlineSession as unknown as { active: TerminalSendOperation | undefined }).active = undefined
    await vi.advanceTimersByTimeAsync(100)
    settle(deadlineOperation)

    const writeTerminal = new FakeTerminal()
    const writeGate = Promise.withResolvers<undefined>()
    writeTerminal.write = async () => { await writeGate.promise }
    const writeSession = new LocalPtySession(writeTerminal, config())
    const writeOperation = writeSession.startSend({ text: 'x', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    ;(writeSession as unknown as { closing: boolean }).closing = true
    writeGate.resolve(undefined)
    await Promise.resolve()
    await Promise.resolve()
    settle(writeOperation)

    const beginTerminal = new FakeTerminal()
    const beginGate = Promise.withResolvers<never>()
    beginTerminal.inspectForeground = async () => await beginGate.promise
    const beginSession = new LocalPtySession(beginTerminal, config())
    const beginOperation = beginSession.startSend({ text: '', submit: false })
    ;(beginSession as unknown as { active: TerminalSendOperation | undefined }).active = undefined
    beginGate.reject(new Error('stale begin failure'))
    await Promise.resolve()
    await Promise.resolve()
    settle(beginOperation)

    const scheduledTerminal = new FakeTerminal()
    const scheduledSession = new LocalPtySession(scheduledTerminal, config())
    const scheduledOperation = scheduledSession.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    const scheduledInternal = scheduledSession as unknown as {
      schedulePoll(operation: TerminalSendOperation, delayMs?: number): void
      settleActive(reason: 'timeout'): void
    }
    scheduledInternal.schedulePoll(scheduledOperation, 5)
    scheduledInternal.settleActive('timeout')
    await scheduledOperation.done

    const pollTerminal = new FakeTerminal()
    const pollSession = new LocalPtySession(pollTerminal, config())
    const pollOperation = pollSession.startSend({ text: '', submit: false })
    await Promise.resolve()
    await Promise.resolve()
    const pollGate = Promise.withResolvers<never>()
    pollTerminal.inspectForeground = async () => await pollGate.promise
    const pollInternal = pollSession as unknown as {
      active: TerminalSendOperation | undefined
      pollReadiness(operation: TerminalSendOperation): Promise<void>
    }
    const stalePoll = pollInternal.pollReadiness(pollOperation)
    pollInternal.active = undefined
    pollGate.reject(new Error('stale poll failure'))
    await stalePoll
    settle(pollOperation)

    const interruptTerminal = new FakeTerminal()
    const interruptGate = Promise.withResolvers<never>()
    interruptTerminal.signalForeground = async () => await interruptGate.promise
    const interruptSession = new LocalPtySession(interruptTerminal, config())
    const interruptOperation = interruptSession.startSend({ text: '', submit: false })
    expect(interruptOperation.cancel()).toBe(true)
    ;(interruptSession as unknown as { active: TerminalSendOperation | undefined }).active = undefined
    interruptGate.reject(new Error('stale interrupt failure'))
    await Promise.resolve()
    await Promise.resolve()
    settle(interruptOperation)
  })
})

describe('LocalPtySession bounds, signals, and teardown', () => {
  it('validates pagination and enforces line/UTF-8 bounds', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(
      terminal,
      config({ scrollbackLines: 3, scrollbackMaxBytes: 12, maxReadBytes: 6 }),
    )
    expect(session.read({})).toMatchObject({ text: '' })
    await initialize(session, terminal)
    const operation = session.startSend({ text: '', submit: false })
    terminal.emitData('一\n二\n三\n四')
    await vi.advanceTimersByTimeAsync(60)
    expect((await operation.done).truncated).toBe(true)
    const page = session.read({ offset: 0, count: 3 })
    expect(Buffer.byteLength(page.text)).toBeLessThanOrEqual(6)
    expect(page.truncated).toBe(true)
    expect(session.read({ offset: 999 })).toMatchObject({ text: '', lineBegin: 999, lineEnd: 999 })
    expect(() => session.read({ offset: -1 })).toThrow('offset')
    expect(() => session.read({ count: 0 })).toThrow('count')

    const tinyTerminal = new FakeTerminal()
    const tiny = new LocalPtySession(tinyTerminal, config({ maxReadBytes: 1 }))
    await initialize(tiny, tinyTerminal)
    const tinyOperation = tiny.startSend({ text: '', submit: false })
    tinyTerminal.emitData('一')
    await vi.advanceTimersByTimeAsync(60)
    await tinyOperation.done
    expect(tiny.read({ offset: 0, count: 1 }).text).toBe('')
  })

  it('signals verified groups and refuses unresolved or shell-targeted hard kills', async () => {
    const terminal = new FakeTerminal()
    const inspector = new FakeInspector()
    const session = makeSession(terminal, inspector, config())
    expect(await session.signal('SIGINT')).toEqual({ delivered: true, targetPgid: 456 })
    inspector.pgid = terminal.pid
    await expect(session.signal('SIGKILL')).rejects.toThrow('terminate the terminal session')
    inspector.pgid = undefined
    await expect(session.signal('SIGTERM')).rejects.toThrow('cannot resolve')
  })

  it('closes idempotently and rejects new signals', async () => {
    const terminal = new FakeTerminal()
    terminal.throwKill = true
    const session = new LocalPtySession(terminal, config({ disposeGraceMs: 1 }))
    const closing = session.close('test')
    expect(session.close('other')).toBe(closing)
    await expect(closing).rejects.toThrow('PTY cleanup failed (test)')
    expect(() => session.startSend({ text: '', submit: false })).toThrow('closing')
    await expect(session.signal('SIGTERM')).rejects.toThrow('closing')
  })

  it('reports cleanup failure without waiting for top-level exit and permits retry', async () => {
    const terminal = new FakeTerminal()
    terminal.autoExitOnKill = false
    terminal.terminateError = new Error('terminal cleanup failed; surviving pids: 456')
    const session = new LocalPtySession(terminal, config())

    await expect(session.close('survivor')).rejects.toMatchObject({
      message: 'PTY cleanup failed (survivor)',
      cause: terminal.terminateError,
    })
    expect(terminal.kills).toEqual([])

    terminal.terminateError = undefined
    terminal.autoExitOnKill = true
    await expect(session.close('retry')).resolves.toBeUndefined()
    expect(terminal.kills).toEqual(['SIGTERM'])
  })

  it('settles an active send as session_exit when closed mid-operation', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config({ disposeGraceMs: 50 }))
    await initialize(session, terminal)
    const operation = session.startSend({ text: 'run', submit: true })
    // The shell returns to its prompt while the send is active; a running
    // readiness poll would otherwise mis-settle this as stdin_read once close
    // begins, so teardown must stop polling before its grace period.
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    terminal.autoExitOnKill = false
    const closing = session.close('mid-send')
    await vi.advanceTimersByTimeAsync(20)
    terminal.emitExit(0, 15)
    expect((await operation.done).waitReason).toBe('session_exit')
    await closing
  })

  it('settles a closing send when provider termination cancels inspection', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    await initialize(session, terminal)
    const write = Promise.withResolvers<undefined>()
    terminal.write = async () => { await write.promise }
    const writeOperation = session.startSend({ text: 'pending write', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    await session.close('pending write')
    expect((await writeOperation.done).waitReason).toBe('session_exit')
    // The rejection lands after close released the send; it must stay contained.
    write.reject(new Error('write rejected during close'))
    await Promise.resolve()
    await Promise.resolve()
  })

  it('settles a closing send when provider termination cancels a pending inspection', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    await initialize(session, terminal)
    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    terminal.inspectForeground = async () => await inspection.promise
    const terminate = terminal.terminate.bind(terminal)
    terminal.terminate = async () => {
      inspection.reject(new Error('terminal terminated'))
      await terminate()
    }
    const operation = session.startSend({ text: 'pending inspection', submit: true })
    await Promise.resolve()

    await session.close('pending inspection')

    expect((await operation.done).waitReason).toBe('session_exit')
    expect(terminal.writes).toEqual([])
  })

  it('does not let an in-flight readiness inspection outrun close', async () => {
    vi.useFakeTimers()
    const terminal = new FakeTerminal()
    const session = new LocalPtySession(terminal, config())
    await initialize(session, terminal)
    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
    const originalInspect = terminal.inspectForeground.bind(terminal)
    let inspections = 0
    terminal.inspectForeground = async () => {
      inspections += 1
      return inspections === 1 ? await originalInspect() : await inspection.promise
    }
    const operation = session.startSend({ text: 'pending readiness', submit: true })
    await Promise.resolve()
    await Promise.resolve()
    terminal.emitData('\x1b]133;D;0\x07dsh> ')
    await vi.advanceTimersByTimeAsync(10)
    expect(inspections).toBe(2)

    const termination = Promise.withResolvers<undefined>()
    terminal.terminate = async () => {
      await termination.promise
      terminal.emitExit(0, 15)
    }
    const closing = session.close('in-flight readiness')
    let settled = false
    void operation.done.then(() => { settled = true })
    inspection.resolve({ processGroupId: 456, inputWaiting: true })
    await Promise.resolve()
    await Promise.resolve()
    expect(settled).toBe(false)

    termination.resolve(undefined)
    await closing
    expect((await operation.done).waitReason).toBe('session_exit')
  })

})
