LogoRobo.js

Scenarios

JSON-based test definitions for reproducible test flows

Automated tests verify individual interactions, but real usage involves multi-step flows. Scenarios let you define complete flows as JSON -- a sequence of dispatch, wait, assert, and interact steps that run against a mock session.

Scenarios are an advanced feature. If you are just getting started with testing, begin with Stage UI for interactive testing or Automated Testing for Jest-based tests.

Stage UI showing a scenario executing with step-by-step progress indicators, each step displaying its type (dispatch, wait, assert, interact) and pass/fail status

FocusThe scenario runner progress view with step-by-step execution statusZoom100%NotesShow a scenario mid-execution or just completed with several steps visible. Each step should show its type badge, description, and pass/fail status. Include the scenario name and overall progress indicator.

Example Scenario

Here is a complete scenario that tests a /ping command:

{
  "version": 1,
  "id": "test-ping-command",
  "metadata": {
    "name": "Ping Command Test",
    "description": "Verifies that /ping responds with Pong",
    "tags": ["commands", "smoke-test"]
  },
  "compatibility": {
    "requiredCommands": ["ping"]
  },
  "steps": [
    {
      "id": "dispatch-ping",
      "type": "dispatch",
      "description": "Run /ping command",
      "dispatch": {
        "kind": "slash_command",
        "payload": { "command_name": "ping" }
      }
    },
    {
      "id": "assert-pong",
      "type": "assert",
      "description": "Bot should respond with Pong",
      "assert": {
        "interactionResponse": {
          "responseType": 4,
          "contentContains": "Pong"
        }
      }
    }
  ]
}

A more complex scenario with component interactions:

{
  "version": 1,
  "id": "test-confirmation-flow",
  "metadata": {
    "name": "Confirmation Flow",
    "description": "Tests a command with a confirm/cancel button"
  },
  "steps": [
    {
      "type": "dispatch",
      "description": "Run /delete-data command",
      "dispatch": {
        "kind": "slash_command",
        "payload": { "command_name": "delete-data" }
      }
    },
    {
      "type": "assert",
      "description": "Should show confirmation buttons",
      "assert": {
        "interactionResponse": {
          "responseType": 4,
          "contentContains": "Are you sure?"
        }
      }
    },
    {
      "type": "interact",
      "description": "Click the confirm button",
      "interact": {
        "componentType": "button",
        "customId": "confirm-delete"
      }
    },
    {
      "type": "assert",
      "description": "Should confirm deletion",
      "assert": {
        "interactionResponse": {
          "contentContains": "deleted"
        }
      }
    }
  ]
}

Overview

A scenario describes:

  • What to send to the bot (commands, messages, button clicks)
  • What to wait for (durations, specific action types)
  • What to verify (assertions on recorded actions)
  • What to interact with (component clicks, select menus, modals)

Scenarios run inside an existing session. Load a scenario via the Control API, start it, and the runner executes each step in order with automatic completion detection between steps.

ScenarioDefinition Structure

Every scenario follows this top-level structure:

{
  "version": 1,
  "id": "unique-scenario-id",
  "metadata": {
    "name": "My Test Scenario",
    "description": "Tests the /ping command responds correctly",
    "tags": ["happy-path", "commands"],
    "author": "dev@example.com"
  },
  "compatibility": {
    "requiredCommands": ["ping"]
  },
  "mockConfig": {
    "user": { "username": "TestUser" },
    "guild": { "name": "Test Server" }
  },
  "steps": []
}
FieldTypeRequiredDescription
version1YesSchema version (only 1 is supported)
idstringYesUnique identifier (UUID recommended)
metadataobjectYesDisplay name, description, tags, author
compatibilityobjectNoPre-flight requirements (commands, options, action types)
mockConfigobjectNoEnvironment setup (user, guild, channel, time config)
stepsarrayYesOrdered sequence of steps to execute

Metadata

interface ScenarioMetadata {
  name: string           // Display name (required)
  description?: string   // Longer description
  tags?: string[]        // For filtering ("happy-path", "edge-case")
  createdAt?: string     // ISO 8601 timestamp
  updatedAt?: string     // ISO 8601 timestamp
  author?: string        // Author identifier
}
// ScenarioMetadata shape:
// {
//   name: string,           // Display name (required)
//   description: string,    // Longer description
//   tags: string[],         // For filtering ("happy-path", "edge-case")
//   createdAt: string,      // ISO 8601 timestamp
//   updatedAt: string,      // ISO 8601 timestamp
//   author: string          // Author identifier
// }

Mock Config

Applied to the session before execution begins:

interface ScenarioMockConfig {
  user?: MockUserConfig        // Invoking user identity
  guild?: MockGuildConfig      // Test guild setup
  channel?: MockChannelConfig  // Test channel setup
  commandOptions?: Record<string, Record<string, unknown>>
  time?: { fixedTime?: number; timeScale?: number }
}
// ScenarioMockConfig shape:
// {
//   user: MockUserConfig,        // Invoking user identity
//   guild: MockGuildConfig,       // Test guild setup
//   channel: MockChannelConfig,   // Test channel setup
//   commandOptions: object,       // { [command]: { [option]: value } }
//   time: { fixedTime: number, timeScale: number }
// }

flashcoreData and apiMocks fields are planned but not yet supported. They are reserved for future use.

Step Types

Each step has a type discriminator and optional base fields:

interface ScenarioStepBase {
  id?: string              // Step identifier for debugging
  description?: string     // Human-readable description
  expectedNodeId?: string  // Source system node ID (for UI highlighting)
  timeout?: number         // Max wait time in ms (overrides default)
}
// ScenarioStepBase shape:
// {
//   id: string,              // Step identifier for debugging
//   description: string,     // Human-readable description
//   expectedNodeId: string,  // Source system node ID (for UI highlighting)
//   timeout: number          // Max wait time in ms (overrides default)
// }

dispatch

Send a Discord gateway event (a simulated Discord message or interaction) to the bot. The dispatch.kind field determines the payload shape.

{
  "type": "dispatch",
  "description": "Run the /ping command",
  "dispatch": {
    "kind": "slash_command",
    "payload": {
      "command_name": "ping"
    }
  }
}

Dispatch Kinds

KindPayload FieldsDescription
slash_commandcommand_name, options?Invoke a slash command
message_createcontent?, channel_id, author?, embeds?, components?Send a user message
button_clickcustom_id, message_idClick a button component
select_optioncustom_id, message_id, valuesSelect from a select menu
modal_submitcustom_id, fields, message_id?Submit a modal form
context_commandcommand_name, target_id, context_typeTrigger a user/message context menu
autocompletecommand_name, focused_option: { name, value, type? }, options?Trigger autocomplete

Each dispatch can also specify channelId, guildId, and user overrides.

Slash command with options:

{
  "type": "dispatch",
  "dispatch": {
    "kind": "slash_command",
    "payload": {
      "command_name": "ban",
      "options": { "user": "123456789", "reason": "Testing" }
    }
  }
}

Message create:

{
  "type": "dispatch",
  "dispatch": {
    "kind": "message_create",
    "payload": {
      "content": "Hello bot!",
      "channel_id": "111222333"
    }
  }
}

Context command:

{
  "type": "dispatch",
  "dispatch": {
    "kind": "context_command",
    "payload": {
      "command_name": "Report User",
      "target_id": "987654321",
      "context_type": 2
    }
  }
}

wait

Pause execution for a fixed duration or until a condition is met.

{
  "type": "wait",
  "description": "Wait for bot response",
  "wait": {
    "forActionType": "interaction_response"
  },
  "timeout": 5000
}

Wait Conditions

ConditionTypeDescription
durationnumberFixed wait time in milliseconds
forActionTypestringWait for a specific action type
forAnyActionTypestring[]Wait for any of the listed action types
forActionobjectWait for an action matching custom criteria

At least one condition must be specified. The forAction matcher supports:

{
  "type": "wait",
  "wait": {
    "forAction": {
      "type": "rest_request",
      "endpointContains": "/channels/",
      "dataContains": { "method": "POST" }
    }
  }
}

If no condition is met before the timeout (default: 5000ms), the step fails.

assert

Verify that the bot produced expected output by checking recorded actions (the history of everything the bot did during the session).

{
  "type": "assert",
  "description": "Bot should reply with pong",
  "assert": {
    "interactionResponse": {
      "responseType": 4,
      "contentContains": "Pong"
    }
  }
}

Assert Checks

CheckDescription
actionRecordedVerify an action of this type was recorded
messageSentCheck that a message was sent matching criteria
interactionResponseCheck that an interaction response was sent

actionRecorded takes an action type string:

{ "assert": { "actionRecorded": "interaction_response" } }

messageSent matcher:

{
  "assert": {
    "messageSent": {
      "contentContains": "Hello",
      "contentMatches": "^Hello.*world$",
      "hasEmbeds": 1,
      "hasComponents": 1,
      "channelId": "111222333"
    }
  }
}

interactionResponse matcher:

{
  "assert": {
    "interactionResponse": {
      "responseType": 4,
      "contentContains": "Success",
      "isEphemeral": true
    }
  }
}

Response type values: 4 = reply, 5 = deferred reply, 6 = deferred update, 7 = update message.

interact

Simulate user interaction with a message component. The runner automatically finds the most recent message containing the specified customId if no messageId is provided.

{
  "type": "interact",
  "description": "Click the confirm button",
  "interact": {
    "componentType": "button",
    "customId": "confirm-action"
  }
}

Component Types

TypeRequired FieldsDescription
buttoncustomIdClick a button
selectcustomId, selectValuesChoose from a select menu
modalcustomId, modalFieldsSubmit a modal form

Select menu:

{
  "type": "interact",
  "interact": {
    "componentType": "select",
    "customId": "role-select",
    "selectValues": ["admin", "moderator"]
  }
}

Modal submit:

{
  "type": "interact",
  "interact": {
    "componentType": "modal",
    "customId": "feedback-form",
    "modalFields": {
      "title": "Bug Report",
      "description": "The bot crashed when..."
    }
  }
}

Run States

A scenario run transitions through these states:

assertion failureresumefatal
idleNo scenario loaded
loadedValidated & ready
runningExecuting steps
completedAll steps passed
pausedCan resume
failedAssertion failure
stoppedManually stopped
errorFatal, not resumable

When a step fails, the runner halts in the failed state. You can resume to continue executing remaining steps or stop the run.

Control API

All scenario endpoints are under /api/control/sessions/:id/scenario.

Load a Scenario

POST /api/control/sessions/:id/scenario

Request body is the full ScenarioDefinition JSON. Returns:

{
  "run_id": "uuid",
  "scenario_id": "your-scenario-id",
  "step_count": 5,
  "warnings": ["Flashcore seeding is not supported yet"]
}

Start Execution

POST /api/control/sessions/:id/scenario/start
{ "mode": "continuous" }

Mode can be "continuous" (run all steps) or "step" (run one step then pause). Default is "continuous".

Pause / Resume / Stop

POST /api/control/sessions/:id/scenario/pause
POST /api/control/sessions/:id/scenario/resume
POST /api/control/sessions/:id/scenario/stop

Each returns the current ScenarioRunState.

Step

POST /api/control/sessions/:id/scenario/step

Execute a single step and return the result. Works from loaded, paused, or failed states.

Seek

POST /api/control/sessions/:id/scenario/seek

Navigate to a previously executed step (for reviewing results). Does not re-execute steps.

Get State

GET /api/control/sessions/:id/scenario/state

Returns the current ScenarioRunState for polling.

Clear Scenario

DELETE /api/control/sessions/:id/scenario

Removes the loaded scenario and resets run state to idle.

Stage UI Integration

When a scenario is running, Stage UI receives real-time events:

  • scenario.run.* - Run lifecycle events (idle, loaded, started, running, paused, resumed, stopped, completed, failed, error)
  • scenario.step.started / scenario.step.completed / scenario.step.failed - Step progress events
  • stage.navigation.changed - Emitted when seek is called, includes snapshot data

These events allow the Stage UI to show step-by-step progress, highlight the current step, and display pass/fail results. The DevTools Tests tab shows scenario run results alongside regular test results.

The runner also captures snapshots at each step boundary. Snapshots store lightweight state (step result summary, last action ID, playback position) for backward navigation using seek.

DevTools Events tab showing scenario-specific events like scenario.step.started, scenario.step.completed, and scenario.run.completed during a scenario run

FocusScenario lifecycle events in the DevTools Events tabZoom100%NotesShow the DevTools Events tab with scenario events visible in the event list. Include events like scenario.run.started, scenario.step.started, scenario.step.completed with their JSON payloads showing step IDs and results.

Next Steps

On this page