Jido.Signal.BusSpy (Jido Signal v2.2.0)

View Source

A test utility for observing signals crossing process boundaries via telemetry events.

The BusSpy allows test processes to capture signals that travel across process boundaries through the Signal Bus without interfering with normal signal delivery. It integrates cleanly with existing cross-process test infrastructure.

Usage

test "cross-process signal observation" do
  # Start the spy to capture bus events
  spy = BusSpy.start_spy()
  
  # Set up your cross-process test scenario
  %{producer: producer, consumer: consumer} = setup_cross_process_agents()
  
  # Send a signal that will cross process boundaries
  send_signal_sync(producer, :root, %{test_data: "cross-process"})
  
  # Wait for completion
  wait_for_cross_process_completion([consumer])
  
  # Verify the signal was observed crossing the bus
  dispatched_signals = BusSpy.get_dispatched_signals(spy)
  assert length(dispatched_signals) == 1
  
  [signal_event] = dispatched_signals
  assert signal_event.signal.type == "child.event"
  assert signal_event.signal.data.test_data == "cross-process"
  
  # Verify trace context was preserved
  assert signal_event.signal.trace_context != nil
  
  BusSpy.stop_spy(spy)
end

Events Captured

The spy captures these telemetry events:

  • [:jido, :signal, :bus, :before_dispatch] - Before signal dispatch
  • [:jido, :signal, :bus, :after_dispatch] - After successful dispatch
  • [:jido, :signal, :bus, :dispatch_skipped] - When middleware skips dispatch
  • [:jido, :signal, :bus, :dispatch_error] - When dispatch fails

The spy prefers full signal and subscription metadata from telemetry when available and falls back to bounded metadata if needed.

Summary

Functions

Returns a specification to start this module under a supervisor.

Clears all captured events from the spy.

Gets all signals that have been dispatched through the bus since the spy started.

Gets the most recent signal event for a specific bus.

Gets signals that match a specific signal type pattern.

Callback implementation for GenServer.init/1.

Starts a new bus spy process to collect telemetry events.

Stops a bus spy process and cleans up telemetry handlers.

Waits for a signal matching the given type pattern to be dispatched.

Types

signal_event()

@type signal_event() :: %{
  event: atom(),
  timestamp: integer(),
  bus_name: atom(),
  signal_id: String.t(),
  signal_type: String.t(),
  subscription_id: String.t(),
  subscription_path: String.t(),
  signal: Jido.Signal.t(),
  subscription: map(),
  dispatch_result: term() | nil,
  error: term() | nil,
  reason: atom() | nil
}

spy_ref()

@type spy_ref() :: pid()

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear_events(spy_ref)

@spec clear_events(spy_ref()) :: :ok

Clears all captured events from the spy.

get_dispatched_signals(spy_ref)

@spec get_dispatched_signals(spy_ref()) :: [signal_event()]

Gets all signals that have been dispatched through the bus since the spy started.

Returns a list of signal events in chronological order.

get_latest_signal(spy_ref, bus_name)

@spec get_latest_signal(spy_ref(), atom()) :: signal_event() | nil

Gets the most recent signal event for a specific bus.

get_signals_by_type(spy_ref, signal_type_pattern)

@spec get_signals_by_type(spy_ref(), String.t()) :: [signal_event()]

Gets signals that match a specific signal type pattern.

Examples

# Get all "user.*" events
user_events = BusSpy.get_signals_by_type(spy, "user.*")

# Get exact matches
child_events = BusSpy.get_signals_by_type(spy, "child.event")

handle_telemetry_event(event_name, measurements, metadata, map)

init(opts)

Callback implementation for GenServer.init/1.

start_spy()

@spec start_spy() :: spy_ref()

Starts a new bus spy process to collect telemetry events.

Returns a spy reference that can be used to query captured events.

stop_spy(spy_ref)

@spec stop_spy(spy_ref()) :: :ok

Stops a bus spy process and cleans up telemetry handlers.

wait_for_signal(spy_ref, signal_type_pattern, timeout \\ 5000)

@spec wait_for_signal(spy_ref(), String.t(), non_neg_integer()) ::
  {:ok, signal_event()} | :timeout

Waits for a signal matching the given type pattern to be dispatched.

Returns the matching signal event or times out.