Simulate: the live run and the dry run

A scenario is an ordered list of what a simulated contact hears, sends and reaches, authored once (conformance/schema/scenario-0.1.schema.json) and run two ways. The live run executes it on an instance. The dry run holds it against the FlowDocs it will run through, on a laptop or in CI, with no instance at all. They answer different questions, and a suite needs both.

The scenario

{
          "scenario": "0.1",
          "name": "keypad-to-appointments",
          "entryPoint": { "channel": "voice", "flow": "${cdref:flow:keypad-line}" },
          "substitutions": [
            {
              "actionType": "TransferContactToQueue",
              "actionParameters": { "QueueId": "${cdref:queue:appointments}" },
              "substitute": "${cdref:queue:overflow}"
            }
          ],
          "steps": [
            { "kind": "expect-prompt", "contains": "Thanks for calling the keypad line" },
            { "kind": "expect-lambda", "lambda": "${cdref:lambda:caller-lookup}" },
            { "kind": "expect-prompt", "contains": "Welcome back, Mrs. Alder" },
            { "kind": "expect-prompt", "contains": "press 1" },
            { "kind": "send-dtmf", "value": "1" },
            { "kind": "expect-hours-check", "hours": "${cdref:hours:main-line}" },
            { "kind": "expect-transfer", "queue": "${cdref:queue:appointments}" },
            {
              "kind": "assert",
              "path": "$.Attributes.callerName",
              "operator": "Equals",
              "value": "Mrs. Alder"
            },
            { "kind": "expect-queue", "queue": "${cdref:queue:appointments}" }
          ]
        }
        

Every resource is a ${cdref:type:name} token, the same token the flow uses, never an ARN and never a console name. expect-transfer and expect-queue both take the queue's token; expect-queue may take name instead when the console name is what the flow reports in $.Queue.Name. The token form of expect-queue asserts on $.Queue.ARN and has not yet been executed against an instance (the name form has); tasks/C14-simulate-offline-checks.md records the run it waits on. The test ends after the last step by default, so the simulated contact never reaches an agent; the queue substitution above is the second safeguard AWS documents.

The dry run

flow-cli simulate --dry-run scenarios/ flows/ seasonal/ --address-map refs/dev.tfmap.json
        

No AWS call, no credentials, no SDK. Every path after the scenarios is read into one set, because a scenario runs across a flow and the modules it calls wherever they live; this is the one command where two directories are not two sets. The suite is validated (schema, then the cross-field rules), and each scenario is then held against the set:

It checks Because otherwise
the entry flow is a document in the set there is nothing to run
a resource an expect-* event waits on is referenced by the set the event can never fire
every other token is referenced by the set or keyed in the map the live run would refuse the suite
each expect-prompt is a text some block plays the observation would wait five minutes and fail
each send-dtmf answers a keypad prompt with a key it takes the flow would take its no-match branch

Only the map's keys are read, so the address map flow-cli emit takes serves as well as a resource map of ARNs, and a scenario whose tokens the set itself references needs no map. A prompt that reads $.Attributes.<name> is heard with the value the scenario starts the contact with or asserts, which is how "Welcome back, Mrs. Alder" matches Welcome back, $.Attributes.callerName. above. SSML is read as its spoken words; a module's text counts like a flow's.

The dry run exits 1 listing every problem, as <scenario file>: <path in the scenario>: <what is wrong>, so the suite is fixed in one round. Exit 0 means nothing offline says a scenario cannot pass.

What it does not do, and why a clean dry run is not a passed scenario:

The checks are @flow-as-code/core's dryRunScenario(scenario, docs, options) and @flow-as-code/cli/simulate's dryRunSimulate(scenarios, flows, options) over files, so a consumer's own tests run the same checks the command does. conformance/simulate/dry-run/ is the contract: a two-document set, a map, and one case per thing the check can say, each with the exact problems it must report.

The live run

flow-cli simulate scenarios/ --instance "$INSTANCE_ARN" --resource-map scenarios/dev.resources.json
        

The resource map holds the ARNs of the deployed resources, keyed any of the three ways the CLI README's "Reference map keys" lists; a run with one unmapped token creates nothing. Each scenario is compiled to the Connect Testing language, created on the instance as a published test case, executed, polled, collected and deleted, within the documented limits (5 concurrent, 100 in flight, 5 minutes per scenario). The report is JUnit or the flow-simulate-report/0.1 JSON; exit 0 only when every scenario PASSED. https://docs.aws.amazon.com/connect/latest/adminguide/testing-simulation-execute-test-cases.html

The live run is the only one that knows what a Lambda returned, which branch the contact took, what the transcript heard and whether the queue reached is the one expected. It costs a deployed environment and AWS credentials, so it is an operator step or a gated job, not a unit test. The dry run is the unit test, and it is what catches the scenario written for a prompt the flow no longer says before the live run spends five minutes timing out on it.

Where each lives

Concern Where
scenario format and validation conformance/schema/scenario-0.1.schema.json, validateScenario
compilation to a test case compileScenario, goldens in conformance/simulate/<case>/
offline checks dryRunScenario, conformance/simulate/dry-run/
the runner and its limits runScenarios, SIMULATE_LIMITS
reports junitReport, jsonReport, conformance/simulate/report/
the command, both runs packages/cli/README.md, "simulate"
what was verified live, when tasks/A06-export-and-simulate.md, tasks/C14-simulate-offline-checks.md