docsPackagessimi — Workflow Tests

simi

simi (Simulation Interface) runs declarative test workflows inside the app. A workflow is a plain object with a list of steps: dispatch a store action, assert on state, wait for a condition or capture a value. The runner executes the steps in order and emits insights events as it goes.

Each run tags its session with simi, the app name, the workflow id and the workflow’s own tags, so test sessions go through the same telemetry and triage tools as real ones.

Exports

ExportWhat it is
defineWorkflow(workflow)Identity helper that type-checks a SimiWorkflow
executeWorkflow(workflow, dispatch, getState, getClient, opts?)The runner. Resolves with a RunResult
KEEP_MODEL_TAG'keep-model'. Workflows with this tag ignore modelOverride
TypesSimiWorkflow, WorkflowStep, ActionStep, AssertStep, WaitForStep, CaptureStep, Resolver, StateResolver, FindResolver, EvalResolver, ResultResolver, RunOpts, RunResult, StepResult

A workflow

import { defineWorkflow } from 'simi';
 
export const basicChatFlow = defineWorkflow({
  id: 'basic_chat_flow',
  app: 'smartchats',
  tags: ['smoke', 'chat'],
  setupWorkflows: ['complete_onboarding'],
  steps: [
    { waitFor: 'state.aiModel !== "" && state.agent !== null', timeout: 10000 },
    { action: 'sendMessageAsync', args: ['What is 2 + 2?'], timeout: 30000, wait: 500 },
    { assert: 'state.chatHistory.length >= 2', message: 'Chat should have user + assistant messages' },
  ],
});

SimiWorkflow fields: id, app, tags?, setupWorkflows? and steps.

Steps

StepFieldsDoes
ActionStepaction, args?, id?, wait?, timeout?Dispatches a store action. wait sleeps afterwards (divided by RunOpts.speed). The step fails if the action’s promise outlasts timeout
AssertStepassert, message?Evaluates the expression. Falsy fails the workflow
WaitForStepwaitFor, timeout?Polls the expression until it is truthy or the timeout passes
CaptureStepcapture, as?Evaluates the expression and records the value in a simi_capture event

Expressions are JavaScript strings evaluated with state (the store state) and results (earlier action results by step label) in scope.

$resolve arguments

Action args can hold resolver objects that are filled in at step time:

{ action: 'editChatMessage', args: [
  { $resolve: 'state', path: 'chatHistory.0.id' },
  'edited content',
] }
 
{ action: 'deleteSession', args: [
  { $resolve: 'find', path: 'sessions', match: { title: 'Untitled' } },
] }
 
{ action: 'apply', args: [
  { $resolve: 'result', step: 'compute_args', path: 'value' },
] }
ResolverFieldsReads
statepathA dotted path in the store state
findpath, match?, index?An item in the array at path
resultstep, path?The return value of an earlier ActionStep with that id
evalfn(state, results?)Whatever the function returns

Setup workflows

setupWorkflows lists workflow ids that run before steps. They are looked up in RunOpts.workflowRegistry. If one is missing or fails, the workflow fails with that error. Setup workflows should be idempotent.

RunOpts

OptionNotes
speedDivides every step wait. Default 1
modelOverrideRuns the workflow on this model: dispatches updateSettings({ aiModel }) first and replaces models pinned by updateSettings steps. Falls back to globalThis.__SMARTCHATS_E2E_MODEL__
workflowRegistryWorkflows by id, for setupWorkflows

Running workflows in the app

createInsightStore in smartchats-common compiles each workflow passed in its workflows config and mounts them on window.__<app>__.simi, keyed by workflow id:

await window.__smartchats__.simi.workflows.basic_chat_flow();
window.__smartchats__.simi.list();   // every workflow id

The Playwright e2e specs in apps/smartchats/tests/e2e/ call workflows the same way. The app’s workflows live in apps/smartchats/app/simi/workflows/ and are registered in apps/smartchats/app/simi/index.ts.

The runner emits simi_workflow_start, simi_step, simi_resolve, simi_capture, simi_model_override and simi_workflow_complete. To pull recent runs, use bin/save_session --app smartchats --tag simi.

See also