test/support/fake_controller.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Support.FakeController do
  @moduledoc """
  Stands in for a connected controller channel process in tests.

  It registers under a machine id exactly as `OpenAgentsWeb.ComputerChannel` does and
  answers `:computer_request` messages from a scripted reply, so the server-side
  request/streaming/cancellation paths can be exercised without a real machine.
  """

  use GenServer

  @doc """
  Starts a fake controller for `machine_id`.

  `:script` is a function receiving `{kind, request_id, payload, caller}` and is
  responsible for sending chunks and one terminal reply. `chunk/3`, `exit/3`,
  `refused/4` build those messages.
  """
  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(options), do: GenServer.start_link(__MODULE__, options)

  @spec chunk(pid(), String.t(), String.t()) :: :ok
  def chunk(caller, request_id, text) do
    send(caller, {:computer_chunk, request_id, text})
    :ok
  end

  @spec exit(pid(), String.t(), map()) :: :ok
  def exit(caller, request_id, payload) do
    send(caller, {:computer_response, request_id, {:ok, payload}})
    :ok
  end

  @spec refused(pid(), String.t(), String.t(), String.t()) :: :ok
  def refused(caller, request_id, reason, detail) do
    send(caller, {:computer_response, request_id, {:refused, reason, detail}})
    :ok
  end

  @impl true
  def init(options) do
    machine_id = Keyword.fetch!(options, :machine_id)
    {:ok, _owner} = OpenAgents.Computer.register(machine_id)

    {:ok,
     %{
       machine_id: machine_id,
       script: Keyword.fetch!(options, :script),
       observer: Keyword.get(options, :observer)
     }}
  end

  @impl true
  def handle_info({:computer_request, kind, request_id, payload, caller}, state) do
    state.script.({kind, request_id, payload, caller})
    {:noreply, state}
  end

  def handle_info({:computer_request, kind, request_id, caller}, state) do
    state.script.({kind, request_id, %{}, caller})
    {:noreply, state}
  end

  def handle_info({:computer_cancel, _request_id} = message, state) do
    if is_pid(state.observer), do: send(state.observer, message)
    {:noreply, state}
  end

  def handle_info(_message, state), do: {:noreply, state}

  @impl true
  def terminate(_reason, state) do
    OpenAgents.Computer.unregister(state.machine_id)
  end
end