telega/testing/graph

Graph export for dialogs and flows — see the whole navigation map before running the bot.

import telega/testing/context
import telega/testing/graph

graph.of_dialog(dialog: booking_dialog(), ctx: context.context(session: Nil))
|> graph.to_dot
|> io.println

Dialog graphs are probed, not guessed. A window’s render is pure and its handlers are pure functions of the state, so the exporter renders every window, presses every button it finds (widget buttons included, routed the way the engine routes them) and records where the returned DialogAction points. Sub-dialogs are entered through the very init/result functions the engine uses, so their windows are probed with real sub state.

Probing sees one state at a time: a Goto that only happens once the state says so is invisible until you probe that state. The same goes for text windows that validate their input — a rejected sample only ever draws the re-render. Pass sample states and texts your handlers accept to of_dialog_probing to widen the sweep.

Probing runs your handlers. Windows are pure by contract, but a handler that writes to a database or calls the API on its way to a Done will do exactly that while the graph is built — so hand it a test context (mock client, test database), never a production one.

Flow graphs are declarative. A flow’s transitions are returned by its handlers (Next, GoTo, …), which are effectful and cannot be probed, so only what the builder knows is drawn: steps, conditionals, parallel fan-out/join, subflows, and the transitions the author declared with flow/builder.declare_next / declare_choice / declare_complete / declare_cancel. Steps left without any declared edge are marked OpaqueNode (dashed) — the honest signal that their navigation is only visible in the handler.

Both graphs are deterministic strings, so they snapshot well:

graph.of_dialog(dialog:, ctx:) |> graph.to_mermaid |> birdie.snap(title: "booking:graph")

Types

pub type Edge {
  Edge(from: String, to: String, label: String, kind: EdgeKind)
}

Constructors

  • Edge(from: String, to: String, label: String, kind: EdgeKind)
pub type EdgeKind {
  Declared
  Probed
  Unknown
}

Constructors

  • Declared

    Declared in the builder: guaranteed to exist regardless of state.

  • Probed

    Found by probing pure handlers with sample states: real, but only as complete as the sampled states.

  • Unknown

    The edge exists, its target is computed at runtime (history-based Back).

A rendered-independent navigation graph. nodes and edges are sorted, so two runs over the same dialog produce byte-identical output.

pub type Graph {
  Graph(name: String, nodes: List(Node), edges: List(Edge))
}

Constructors

  • Graph(name: String, nodes: List(Node), edges: List(Edge))
pub type Node {
  Node(
    id: String,
    label: String,
    kind: NodeKind,
    group: option.Option(String),
  )
}

Constructors

pub type NodeKind {
  EntryNode
  StepNode
  InputNode
  OpaqueNode
  TerminalNode
  ExternalNode
}

Constructors

  • EntryNode

    Synthetic start marker.

  • StepNode

    A dialog window or a flow step.

  • InputNode

    A dialog window that also accepts text input.

  • OpaqueNode

    A flow step whose transitions are decided inside the handler and cannot be extracted statically.

  • TerminalNode

    Synthetic end marker (done, back, subflow return).

  • ExternalNode

    A URL or web-app target outside the bot.

Values

pub fn default_texts() -> List(String)

Default sample text fed to on_text windows while probing.

pub fn of_dialog(
  dialog dialog: dialog.Dialog(
    state,
    session,
    error,
    dependencies,
  ),
  ctx ctx: bot.Context(session, error, dependencies),
) -> Graph

Build the navigation graph of a dialog, probing every window with its initial state.

pub fn of_dialog_probing(
  dialog dialog: dialog.Dialog(
    state,
    session,
    error,
    dependencies,
  ),
  ctx ctx: bot.Context(session, error, dependencies),
  states states: List(state),
  texts texts: List(String),
) -> Graph

Build the navigation graph of a dialog, probing every window with the initial state plus states, and every text window with texts.

Extra states uncover state-dependent navigation: a window that renders a “continue” button only once a choice is made contributes its edge only when probed with a state that has one.

pub fn of_flow(
  flow flow: types.Flow(step_type, session, error, dependencies),
) -> Graph

Build the declarative skeleton of a flow: steps, conditional branches, parallel fan-out/join and subflows.

Transitions returned by step handlers (Next, GoTo, Complete) are not visible here — handlers are effectful and are never called. Steps with no declared outgoing edge are marked OpaqueNode.

pub fn to_dot(graph graph: Graph) -> String

Render the graph as Graphviz DOT. Pipe it to dot -Tsvg.

pub fn to_mermaid(graph graph: Graph) -> String

Render the graph as a Mermaid flowchart. Renders inline in GitHub and in the docs without a local Graphviz.

Search Document