import { describe, expect, it } from 'vitest'
import { ApprovalRequestId } from '../src/index.ts'
import type {
  ApprovalRequestId as ApprovalRequestIdType, CordisDynamicPluginId,
} from '../src/types.ts'
import { AGENT_A, AGENT_B, CLIENT_CODE, setup, running } from './helpers.ts'

/**
 * The runner's own chain on a real cordis tree: define records without running,
 * run starts a real host-half fiber and broadcasts one request, the first answer
 * settles it, and stop/undefine unwind both halves. Only the model and the
 * browser are stand-ins (code strings and a recording gateway).
 */

/** A host half that registers one invoke handler and provides a service. */
const HOST_CODE = `
  harness.handle('double', async (args) => args.value * 2)
  return {
    name: 'doubler',
    apply(ctx) {
      ctx.provide('dynDoubler', { ok: true })
    },
  }
`

type Runner = Awaited<ReturnType<typeof setup>>['runner']

function define(
  runner: Runner,
  request: {
    sessionId: typeof AGENT_A.id
    name: string
    purpose: string
    host?: string
    client?: string
  },
) {
  return runner.define({
    sessionId: request.sessionId,
    plugin: { kind: 'new', idPrefix: 'dyn' },
    name: request.name,
    purpose: request.purpose,
    code: {
      ...request.host === undefined ? {} : { host: request.host },
      ...request.client === undefined ? {} : { client: request.client },
    },
  })
}

describe('dynamic runner definitions', () => {
  it('lists the whole registry for a global surface, each row carrying its owning session', async () => {
    const { runner } = await setup()
    const mine = define(runner, { sessionId: AGENT_A.id, name: 'mine', purpose: 'ours', host: HOST_CODE })
    const theirs = define(runner, { sessionId: AGENT_B.id, name: 'theirs', purpose: 'not ours', client: CLIENT_CODE })

    // Global by design: a run-control surface that is not inside a session can
    // still name every package, and each row carries the address later verbs need.
    expect(runner.inventory()).toEqual([
      {
        pluginId: mine.pluginId,
        agentId: AGENT_A.id,
        packages: [{
          packageId: mine.packageId, name: 'mine', purpose: 'ours', hasHostHalf: true, hasClientHalf: false,
        }],
      },
      {
        pluginId: theirs.pluginId,
        agentId: AGENT_B.id,
        packages: [{
          packageId: theirs.packageId, name: 'theirs', purpose: 'not ours', hasHostHalf: false, hasClientHalf: true,
        }],
      },
    ])
    // Authority did not move with the listing: acting still needs the owner.
    await expect(runner.run(AGENT_A, theirs.pluginId, theirs.packageId, 'run'))
      .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' })
  })

  it('tells a global surface which definitions even have a browser half to load', async () => {
    const { runner } = await setup()
    define(runner, { sessionId: AGENT_A.id, name: 'host only', purpose: 'no UI', host: HOST_CODE })
    define(runner, {
      sessionId: AGENT_A.id,
      name: 'both halves',
      purpose: 'UI too',
      host: HOST_CODE,
      client: CLIENT_CODE,
    })

    // A host-only package cannot be loaded into a page, so the surface must be
    // able to tell the two apart from the listing alone.
    expect(runner.inventory().map(row => [String(row.pluginId), row.packages[0]?.hasClientHalf])).toEqual([
      ['dyn-1', false],
      ['dyn-2', true],
    ])
  })

  it('records a definition without running it, and mints ids that are never reused', async () => {
    const { runner } = await setup()

    const first = define(runner, { sessionId: AGENT_A.id, name: 'first', purpose: 'do a thing', host: HOST_CODE })
    const second = define(runner, { sessionId: AGENT_A.id, name: 'second', purpose: 'do another', client: 'return () => {}' })

    expect(first).toEqual({
      pluginId: 'dyn-1', packageId: 'pkg-1', name: 'first', purpose: 'do a thing',
      hasHostHalf: true, hasClientHalf: false,
    })
    expect(second).toEqual({
      pluginId: 'dyn-2', packageId: 'pkg-2', name: 'second', purpose: 'do another',
      hasHostHalf: false, hasClientHalf: true,
    })
    expect(running(runner, AGENT_A)).toEqual([{ id: 'dyn-1', running: false }, { id: 'dyn-2', running: false }])
  })

  it.each([
    [{ name: ' ', purpose: 'p', host: 'return () => {}' }, 'non-empty `name`'],
    [{ name: 'n', purpose: '', host: 'return () => {}' }, 'non-empty `purpose`'],
    [{ name: 'n', purpose: 'p' }, 'needs `code.host`, `code.client`, or both'],
  ])('refuses an incomplete define request: %j', async (request, message) => {
    const { runner } = await setup()
    expect(() => define(runner, { sessionId: AGENT_A.id, ...request })).toThrow(message)
  })

  it('keeps unparseable code out of the registry, teaching the TypeScript removal', async () => {
    const { runner } = await setup()

    expect(() => define(runner, {
      sessionId: AGENT_A.id,
      name: 'broken',
      purpose: 'p',
      client: 'return { type: \'text\' as const }',
    })).toThrow('The sandbox runs plain JavaScript, not TypeScript')
    expect(running(runner, AGENT_A)).toEqual([])
  })

  it('hides another session\'s definition, so only its own card can address it', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'owned', purpose: 'p', host: HOST_CODE,
    })

    expect(running(runner, AGENT_B)).toEqual([])
    await expect(runner.run(AGENT_B, pluginId, packageId, 'run'))
      .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' })
    await expect(runner.stop(AGENT_B, pluginId)).resolves.toMatchObject({ ok: false, reason: 'plugin-missing' })
  })
})

describe('dynamic runner dispatch', () => {
  it('starts a host-only package immediately, with no request and no approval', async () => {
    const { ctx, runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toEqual({
      ok: true,
      status: 'running',
      pluginId,
      packageId,
      pluginRunId: 'run-1',
      waitingFor: [],
      currentPackageId: packageId,
      mode: 'run',
    })
    expect(ctx.get('dynDoubler')).toEqual({ ok: true })
    // Its own business: the only announcement is the run-state one.
    expect(gateway.events).toEqual([
      ['cordis/dynamic-package', { pluginId, packageId, pluginRunId: 'run-1', name: 'doubler' }],
    ])
    await expect(runner.invoke(pluginId, 'run-1' as never, 'double', { value: 21 }))
      .resolves.toEqual({ ok: true, value: 42 })
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: true }])
  })

  it('returns awaiting approval, then records the page activation asynchronously', async () => {
    const { ctx, runner, gateway } = await setup()
    gateway.answer = 'approve'
    gateway.clientWaitingFor = ['slots']
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE,
    })

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toEqual({
      ok: true,
      status: 'awaiting-approval',
      pluginId,
      packageId,
      pluginRunId: 'run-1',
      mode: 'run',
      waitingFor: [],
      nextPackageId: packageId,
    })
    await gateway.answering
    expect(ctx.get('dynDoubler')).toEqual({ ok: true })
    expect(runner.inventory()[0]?.latestRun).toMatchObject({
      status: 'waiting',
      client: { status: 'waiting', waitingFor: ['slots'] },
    })
    expect(gateway.events.map(([name]) => name)).toEqual([
      'cordis/request-run', 'cordis/dynamic-package', 'cordis/request-run-resolved',
    ])
    expect(gateway.events.at(-1)?.[1]).toMatchObject({ outcome: 'approved' })
  })

  it('returns awaiting approval, then records a refusal without starting', async () => {
    const { ctx, runner, gateway } = await setup()
    gateway.answer = 'reject'
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE,
    })

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toMatchObject({ ok: true, status: 'awaiting-approval' })
    await gateway.answering
    expect(ctx.get('dynDoubler')).toBeUndefined()
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }])
    expect(gateway.events.at(-1)).toMatchObject(['cordis/request-run-resolved', { outcome: 'rejected' }])
  })

  it('records an asynchronous Client failure and unwinds the Host half it started', async () => {
    const { ctx, runner, gateway } = await setup()
    gateway.answer = { clientFails: 'createElement is not defined' }
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE,
    })

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toMatchObject({ ok: true, status: 'awaiting-approval' })
    await gateway.answering
    expect(runner.inventory()[0]?.latestRun).toMatchObject({
      status: 'failed',
      error: { message: 'createElement is not defined' },
    })
    // Rollback restores the state the request found: nothing was running before.
    expect(ctx.get('dynDoubler')).toBeUndefined()
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }])
    expect(gateway.events.at(-1)?.[1]).toMatchObject({ outcome: 'failed' })
  })

  it('replaces a prior run and records failure when the repeated run cannot load Client code', async () => {
    const { ctx, runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE,
    })
    // A first page ran it; a second request finds it already up.
    gateway.answer = 'approve'
    await runner.run(AGENT_A, pluginId, packageId, 'run')
    await gateway.answering
    gateway.answer = { clientFails: 'this page could not load it' }

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toMatchObject({ ok: true, status: 'starting' })
    await gateway.answering
    expect(runner.inventory()[0]?.latestRun).toMatchObject({
      status: 'failed',
      error: { message: 'this page could not load it' },
    })
    expect(ctx.get('dynDoubler')).toBeUndefined()
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }])
  })

  it('binds a running host half instead of evaluating it twice', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })

    const first = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)
    const second = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)

    expect(first).toEqual({
      ok: true, pluginId, packageId, pluginRunId: 'run-1', waitingFor: [], startedHere: true,
    })
    // Re-evaluating would collide on the provided service; binding is what lets
    // a reloaded page take a live package back.
    expect(second).toEqual({
      ok: true, pluginId, packageId, pluginRunId: 'run-1', waitingFor: [], startedHere: false,
    })
  })

  it('shares one activation when two pages start the same Package concurrently', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })

    const [first, second] = await Promise.all([
      runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false),
      runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false),
    ])

    expect(first).toMatchObject({ ok: true, pluginRunId: 'run-1', startedHere: true })
    expect(second).toEqual(first)
  })

  it('hands the browser half\'s source only to the owning session, and only while it runs', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE,
    })

    expect(() => runner.getClientCode(AGENT_A, pluginId, 'run-0' as never)).toThrow('is not running')
    const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)
    if (!started.ok) throw new Error(started.message)
    expect(runner.getClientCode(AGENT_A, pluginId, started.pluginRunId)).toEqual({
      code: CLIENT_CODE, name: 'ui', pluginId, packageId, pluginRunId: started.pluginRunId,
    })
    expect(() => runner.getClientCode(AGENT_B, pluginId, started.pluginRunId)).toThrow('no dynamic plugin')
  })

  it('accepts and ignores an answer to a request nobody is waiting for', async () => {
    const { runner } = await setup()

    await expect(runner.resolveRequestRun(ApprovalRequestId('approval-404'), {
      ok: true, pluginRunId: 'run-1' as never,
    }))
      .resolves.toEqual({ accepted: false })
  })

  it('refuses an answer after stop cancels the request and allows a fresh direct run', async () => {
    const { runner, gateway } = await setup()
    // No auto-answer: this suite drives the round trip by hand so the dispatch
    // can be replaced underneath the page that is still loading run 1.
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE,
    })
    const pending = await runner.run(AGENT_A, pluginId, packageId, 'run')
    expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' })
    await Promise.resolve()
    const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1]
    const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId
    const first = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false)
    if (!first.ok) throw new Error(first.message)
    expect(runner.getClientCode(AGENT_A, pluginId, first.pluginRunId).pluginRunId).toBe(first.pluginRunId)
    // The user stops it while that page is still loading, cancelling the request.
    await runner.stop(AGENT_A, pluginId)

    await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: first.pluginRunId }))
      .resolves.toEqual({ accepted: false })
    expect(gateway.events).toContainEqual([
      'cordis/request-run-resolved',
      { requestId, outcome: 'cancelled' },
    ])
    await expect(runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false))
      .resolves.toMatchObject({ ok: true, pluginRunId: 'run-2', startedHere: true })
  })

  it('cancels a pending request after its provisional activation is stopped', async () => {
    const { runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE,
    })
    const controller = new AbortController()
    const pending = await runner.run(AGENT_A, pluginId, packageId, 'run', controller.signal)
    expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' })
    await Promise.resolve()
    const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1]
    const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId
    const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false)
    if (!started.ok) throw new Error(started.message)
    await runner.stop(AGENT_A, pluginId)

    await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: started.pluginRunId }))
      .resolves.toEqual({ accepted: false })
    controller.abort()
    expect(gateway.events).toContainEqual([
      'cordis/request-run-resolved',
      { requestId, outcome: 'cancelled' },
    ])
  })

  it('keeps a published request answerable after the creating Tool call ends', async () => {
    const { runner, gateway } = await setup()
    // No answer configured: the request stays pending until the signal fires.
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE,
    })
    const controller = new AbortController()

    const pending = await runner.run(AGENT_A, pluginId, packageId, 'run', controller.signal)
    await Promise.resolve()
    controller.abort()

    expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' })
    const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1]
    const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId
    const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false)
    if (!started.ok) throw new Error(started.message)
    await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: started.pluginRunId }))
      .resolves.toEqual({ accepted: true })
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: true }])
  })

  it('reports the sandbox failure and starts nothing when the host half throws', async () => {
    const { runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id,
      name: 'broken',
      purpose: 'p',
      host: 'harness.handle(\'never\', async () => 1)\nthrow new Error(\'host half exploded\')',
    })

    const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run')

    expect(receipt).toMatchObject({ ok: false, reason: 'host-half-failed' })
    expect(gateway.events).toEqual([])
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }])
    await expect(runner.invoke(pluginId, 'run-1' as never, 'never', null))
      .resolves.toMatchObject({ code: 'plugin-not-running' })
  })
})

describe('dynamic runner teardown', () => {
  it('stops both halves while keeping the definition runnable', async () => {
    const { ctx, runner, gateway } = await setup()
    gateway.answer = 'approve'
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE,
    })
    const first = await runner.run(AGENT_A, pluginId, packageId, 'run')
    if (!first.ok) throw new Error(first.message)
    await gateway.answering

    await expect(runner.stop(AGENT_A, pluginId)).resolves.toEqual({ ok: true })

    expect(ctx.get('dynDoubler')).toBeUndefined()
    await expect(runner.invoke(pluginId, first.pluginRunId, 'double', { value: 1 }))
      .resolves.toMatchObject({ code: 'plugin-not-running' })
    expect(gateway.events.at(-1)).toEqual(['cordis/dynamic-retract', {
      pluginId, packageId, pluginRunId: first.pluginRunId,
    }])
    expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }])
    // Runnable again, with a fresh activation identity.
    await expect(runner.run(AGENT_A, pluginId, packageId, 'run'))
      .resolves.toMatchObject({ ok: true, pluginRunId: 'run-2' })
  })

  it('announces the stop of a host-only package too, so a global surface drops its row', async () => {
    const { runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })
    await runner.run(AGENT_A, pluginId, packageId, 'run')

    await expect(runner.stop(AGENT_A, pluginId)).resolves.toEqual({ ok: true })

    // The retract mirrors the start announcement: a run-control surface tracks
    // "is it running", which is independent of whether a browser half existed.
    expect(gateway.events).toEqual([
      ['cordis/dynamic-package', { pluginId, packageId, pluginRunId: 'run-1', name: 'doubler' }],
      ['cordis/dynamic-retract', { pluginId, packageId, pluginRunId: 'run-1' }],
    ])
  })

  it('refuses to stop a definition that is not running', async () => {
    const { runner } = await setup()
    const { pluginId } = define(runner, {
      sessionId: AGENT_A.id, name: 'idle', purpose: 'p', host: HOST_CODE,
    })

    await expect(runner.stop(AGENT_A, pluginId)).resolves.toMatchObject({ ok: false, reason: 'not-running' })
  })

  it('stops a running definition on undefine and forgets it', async () => {
    const { ctx, runner, gateway } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })
    await runner.run(AGENT_A, pluginId, packageId, 'run')

    await expect(runner.undefine(AGENT_A, pluginId)).resolves.toEqual({ ok: true, wasRunning: true })

    expect(ctx.get('dynDoubler')).toBeUndefined()
    expect(running(runner, AGENT_A)).toEqual([])
    expect(gateway.events.map(([name]) => name))
      .toEqual(['cordis/dynamic-package', 'cordis/dynamic-retract'])
    await expect(runner.run(AGENT_A, pluginId, packageId, 'run'))
      .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' })
  })

  it('answers a missing definition with the memory-only explanation', async () => {
    const { runner } = await setup()
    const receipt = await runner.undefine(AGENT_A, 'dyn-404' as CordisDynamicPluginId)

    expect(receipt).toMatchObject({ ok: false, reason: 'plugin-missing' })
    expect((receipt as { message: string }).message).toContain('lost on DSH restart')
  })

  it('unwinds every host half when the runner itself is disposed', async () => {
    const { ctx, runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE,
    })
    await runner.run(AGENT_A, pluginId, packageId, 'run')
    expect(ctx.get('dynDoubler')).toEqual({ ok: true })

    await ctx.fiber.dispose()

    expect(ctx.get('dynDoubler')).toBeUndefined()
  })
})

describe('render failure reports', () => {
  it('keeps the last report per package and shows it to a snapshot reader', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', client: CLIENT_CODE,
    })
    const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)
    if (!started.ok) throw new Error(started.message)

    await runner.reportRenderFailure(
      AGENT_A, pluginId, started.pluginRunId,
      { slot: 'settings.section', message: 'boom', abdicated: true },
    )
    // Cross-page and last-writer-wins: a second page reporting overwrites,
    // because "did this package's UI fail anywhere" has one answer.
    await runner.reportRenderFailure(
      AGENT_A, pluginId, started.pluginRunId,
      { slot: 'shell.overlay', message: 'later', abdicated: false },
    )

    expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure)
      .toEqual({ slot: 'shell.overlay', message: 'later', abdicated: false })
  })

  it('drops a report for a definition the reporting session does not own', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', client: CLIENT_CODE,
    })
    const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)
    if (!started.ok) throw new Error(started.message)

    // The reporting path must never fail a render, so a report it cannot place
    // is dropped rather than thrown.
    await expect(runner.reportRenderFailure(
      AGENT_B, pluginId, started.pluginRunId, { slot: 's', message: 'm', abdicated: true },
    ))
      .resolves.toBeNull()
    expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined()
  })

  it('clears the report when a fresh dispatch starts and when one stops', async () => {
    const { runner } = await setup()
    const { pluginId, packageId } = define(runner, {
      sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', host: 'return () => {}',
    })
    const first = await runner.run(AGENT_A, pluginId, packageId, 'run')
    if (!first.ok) throw new Error(first.message)
    await runner.reportRenderFailure(
      AGENT_A, pluginId, first.pluginRunId,
      { slot: 'settings.section', message: 'boom', abdicated: true },
    )
    expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeDefined()

    // Stop clears it: nothing is mounted to have failed any more.
    await runner.stop(AGENT_A, pluginId)
    expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined()

    await runner.reportRenderFailure(
      AGENT_A, pluginId, first.pluginRunId,
      { slot: 'settings.section', message: 'boom', abdicated: true },
    )
    // A fresh dispatch clears it too: a failure from the previous run would
    // describe something that is no longer there.
    await runner.run(AGENT_A, pluginId, packageId, 'run')
    expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined()
  })
})
