A handshake

Run a process with multiple actors, actions and responses.

In the previous tutorial we created our first Live Contract and tested it with lctest. The scenario only defined a single actor. In most workflows (especially decentralized once), multiple actors participate on the process.

Create a new handshake subdirectory and copy scenario.yml from the basic directory.

Adding the recipient actor

Let's expand the scenario for a process where two actors greet each other. We start with our previous scenario and add the recipient actor.

schema: https://specs.letsflow.io/v0.3.0/scenario
title: A handshake

actors:
  initiator:
    title: Initiator
  recipient:
    title: Recipient

actions:
  complete:
    actor: initiator

states:
  initial:
    on: complete
    goto: (success)

Greeting each other

We want the two actors to interact in the form of a simple greeting:

Initiator: Hi, how are you? Recipient: Fine. How about you? Initiator: Fine

The initiator will still complete the process, but from the initial state it will first do a greeting, expecting a reply. We'll add these 2 actions and subsequently the states where these can be performed. First the initiator waits on the recipient and then visa versa.

schema: https://specs.letsflow.io/v0.3.0/scenario
title: A handshake

actors:
  initiator:
    title: Initiator
  recipient:
    title: Recipient

actions:
  greet:
    actor: initiator
  reply:
    actor: recipient
  complete:
    actor: initiator

states:
  initial:
    on: greet
    goto: wait_on_recipient
  wait_on_recipient:
    on: reply
    goto: wait_on_initiator
  wait_on_initiator:
    on: complete
    goto: (success)

We see that we transition from the initial state to wait_on_recipient, than wait on initiator and finally to a successful completion of the proces.

Running the test

As before, we want to create a run a test to make sure the process runs as expected.

main.feature
Feature: Two actors greet each other

  Background:
    Given a chain is created by "Joe"
    Given "Joe" creates the "main" process using the "handshake" scenario
    And   "Joe" is the "initiator" actor of the "main" process
    And   "Jane" is the "recipient" actor of the "main" process

  Scenario:
    When "Joe" runs the "greet" action of the "main" process
    Then the "main" process is in the "wait_on_recipient" state
    When "Jane" runs the "reply" action of the "main" process
    Then the "main" process is in the "wait_on_initiator" state
    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

Choose to ignore

While it's nice to reply, the recipient may also choose to ignore the greeting. In this the process will be cancelled. Let's add an ignore action and make is possible to either reply or ignore in the wait on recipient state.

schema: https://specs.letsflow.io/v0.3.0/scenario
title: A handshake

actors:
  initiator:
    title: Initiator
  recipient:
    title: Recipient

actions:
  greet:
    actor: initiator
  reply:
    actor: recipient
  ignore:
    actor: recipient
  complete:
    actor: initiator

states:
  initial:
    on: greet
    goto: wait_on_recipient
  wait_on_recipient:
    transitions:
      - on: reply
        goto: wait_on_initiator
      - on: ignore
        goto: :cancelled
  wait_on_initiator:
    on: complete
    goto: (success)

Rather than a single action property, the wait_on_recipient state now has an actions property that contains an array with the actions that be performed. The transition property has been replaced with a transitions property, defining the transition for each action.

Testing the scenario

We can add a new Scenario section in our test file to test the path in the process where the recipient "Jane" ignores the greeting.

main.feature
Feature: Two actors greet each other

  Background:
    Given a chain is created by "Joe"
    Given "Joe" creates the "main" process using the "handshake" scenario
    And   "Joe" is the "initiator" actor of the "main" process
    And   "Jane" is the "recipient" actor of the "main" process

  Scenario:
    When "Joe" runs the "greet" action of the "main" process
    Then the "main" process is in the "wait_on_recipient" state
    When "Jane" runs the "reply" action of the "main" process
    Then the "main" process is in the "wait_on_initiator" state
    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

  Scenario:
    When "Joe" runs the "greet" action of the "main" process
    Then the "main" process is in the "wait_on_recipient" state
    When "Jane" runs the "ignore" action of the "main" process
    Then the "main" process is cancelled

You can have multiple Scenario sections. The Background runs once and than make a backup of the databases. After each scenario the data in the database is reverted, to run the next scenario.

A conversation

In this process Jane (the recipient) can only ignore the greeting or respond with "fine". But unfortunately it's not always sunshine and rainbows. It's possible to define different responses for an action.

Up until now, we've created the scenario first and added a test later. It's recommended to take a TDD style approach, where you start with a test and then write or modify your scenario.

Jane might answer with "not so good", asking for sympathy from Joe. He might still end the conversation (complete) by answering with "Sorry to hear that" and move on. A nicer thing to is is to ask "What's the matter?" upon which Jane can give a response.

Testing the scenario

We could add a new Scenario section to our main.feature test file, but instead we'll create a new test file recipient-not-good.feature specifically for the use case where the greeting turns into a bigger conversation.

We already have two paths we want to test. One where Joe brushes Jane off and one where he's genuinely interested. We'll expand the background to the point where Jane give her response.

recipient-not-good.feature
Feature: Two actors meet. The recipient is not doing well.

  Background:
    Given a chain is created by "Joe"
    Given "Joe" creates the "main" process using the "handshake" scenario
    And   "Joe" is the "initiator" actor of the "main" process
    And   "Jane" is the "recipient" actor of the "main" process
    When "Joe" runs the "greet" action of the "main" process
    Then the "main" process is in the "wait_on_recipient" state

  Scenario:
    When "Jane" runs the "reply" action of the "main" process giving a "not_good" response
    Then the "main" process is in the "expect_sympathy" state
    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

  Scenario:
    When "Jane" runs the "reply" action of the "main" process giving a "not_good" response
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "sympathize" action of the "main" process
    Then the "main" process is in the "recipient_can_elaborate" state
    When "Jane" runs the "elaborate" action of the "main" process with:
      | reason | My cat is stealing my boyfriend. She pnly cuddles with him. |
    Then the "main" process is in the "wait_on_initiator" state

    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

When the recipient elaborates, she'll send the reason as additional response data. In the tutorial "A proper introduction" we'll see how to use the response data in the process.

The scenario

When we run this test we can see that it fails. The not good response hasn't been defined. We also need to define the sympathize and elaborate actions and the expect sympathy and recipient can elaborate states.

schema: https://specs.letsflow.io/v0.3.0/scenario
title: A handshake

actors:
  initiator:
    title: Initiator
  recipient:
    title: Recipient

actions:
  greet:
    actor: initiator
  reply:
    actor: recipient
    responses:
      ok:
      not_good:
  ignore:
    actor: recipient
  sympathize:
    actor: initiator
  elaborate:
    actor: recipient
  complete:
    actor: initiator

states:
  initial:
    on: greet
    goto: wait_on_recipient
  wait_on_recipient:
    transitions:
      - on: reply.ok
        goto: wait_on_initiator
      - on: reply.not_good
        goto: expect_sympathy
      - on: ignore
        goto: (cancelled)
  expect_sympathy:
    transitions:
      - on: sympathize
        goto: recipient_can_elaborate
      - on: complete
        goto: (success)
  recipient_can_elaborate:
    on: elaborate
    goto: wait_on_initiator
  wait_on_initiator:
    on: complete
    goto: (success)

The default response has the key "ok" by default. If we define a set of responses without "ok", we also need to set the default_response property of the action.

Keeping the conversation going

Instead of asking "What's the matter?", Joe could say "Sorry to hear that. Please tell me more". By repeating the sympathize action, Joe can keep the conversation going.

recipient-not-good.feature
Feature: Two actors meet. The recipient is not doing well.

  Background:
    Given a chain is created by "Joe"
    Given "Joe" creates the "main" process using the "handshake" scenario
    And   "Joe" is the "initiator" actor of the "main" process
    And   "Jane" is the "recipient" actor of the "main" process
    When "Joe" runs the "greet" action of the "main" process
    Then the "main" process is in the "wait_on_recipient" state

  Scenario:
    When "Jane" runs the "reply" action of the "main" process giving a "not_good" response
    Then the "main" process is in the "expect_sympathy" state
    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

  Scenario:
    When "Jane" runs the "reply" action of the "main" process giving a "not_good" response
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "sympathize" action of the "main" process
    Then the "main" process is in the "recipient_can_elaborate" state
    When "Jane" runs the "elaborate" action of the "main" process with:
      | reason | My cat is stealing my boyfriend. |
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

  Scenario:
    When "Jane" runs the "reply" action of the "main" process giving a "not_good" response
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "sympathize" action of the "main" process
    Then the "main" process is in the "recipient_can_elaborate" state
    When "Jane" runs the "elaborate" action of the "main" process with:
      | reason | My cat is stealing my boyfriend. |
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "sympathize" action of the "main" process
    Then the "main" process is in the "recipient_can_elaborate" state
    When "Jane" runs the "elaborate" action of the "main" process with:
      | reason | She always comes in to cuddle with him. |
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "sympathize" action of the "main" process
    Then the "main" process is in the "recipient_can_elaborate" state
    When "Jane" runs the "elaborate" action of the "main" process with:
      | reason | Misty has a mean purr. I know it's to taunt me. |
    Then the "main" process is in the "expect_sympathy" state

    When "Joe" runs the "complete" action of the "main" process
    Then the "main" process is completed

Modifying the scenario

To do so, we just need to modify the recipient can elaborate state to transition to expect sympathy. Since the initiator can also complete the process from that state, it doesn't affect our existing use cases.

Previous tests should be, of course, also modified.

  recipient_can_elaborate:
    on: elaborate
    goto: expect_sympathy

Instructions and titles

As briefly show in the previous tutorial, actors and actions can have a title. We can also provide a title for states and responses. These titles won't affect the process, but can be used in a UI to display to the end users. Additionally it will make the scenario a bit less cryptic.

The title should tell what we're waiting on. The title of the action describes what an actor will do, while the title of the response describes what the actor has done (or has said in this example).

Instructions for a specific actor can be defined for each state. Again, this doesn't influence the process, but can be used in a UI.

schema: https://specs.letsflow.io/v0.3.0/scenario
title: A handshake

actors:
  initiator:
    title: Initiator
  recipient:
    title: Recipient

actions:
  greet:
    actor: initiator
    title: Greet the person you're meeting
    responses:
      ok:
        title: Hi, how are you?
  reply:
    actor: recipient
    title: Respond to the greeting
    responses:
      ok:
        title: Fine. How about you?
      not_good:
        title: Not so good.
  ignore:
    actor: recipient
    title: Ignore the greeting
  sympathize:
    actor: initiator
    title: Ask further
    responses:
      ok:
        title: Sorry to hear that. Please tell me more.
  elaborate:
    actor: recipient
    title: Tell what's the matter.
  complete:
    actor: initiator
    title: End the conversation

states:
  initial:
    action: greet
    transition: wait_on_recipient
  wait_on_recipient:
    title: Waiting on the recipient to respond.
    instructions:
      recipient: Respond or, if you're feeling rude, ignore it.
    actions:
      - reply
      - ignore
    transitions:
      - action: reply
        response: ok
        transition: wait_on_initiator
      - action: reply
        response: not_good
        transition: expect_sympathy
      - action: ignore
        transition: :cancelled
  expect_sympathy:
    title: Waiting on the initiator to respond.
    instructions:
      initiator: Ask further or end the conversation politely.
    actions:
      - sympathize
      - complete
    transitions:
      - action: sympathize
        transition: recipient_can_elaborate
      - action: complete
        transition: :success
  recipient_can_elaborate:
    title: Waiting on the recipient to elaborate.
    instructions:
      recipient: Please explain why it's not going well.
    action: elaborate
    transition: expect_sympathy
  wait_on_initiator:
    title: Waiting on the initiator to respond.
    action: complete
    transition: :success

Adding titles and instructions is optional. While it makes the scenario less cryptic. It also substantially increase lines of code of the scenario.

We didn't specify a title for the complete action. That's because the response that would giving in the wait on initiator state would would "Fine". In the state where we expect sympathy from initiator would the response for the same action would be "Sorry, to hear that".

We could define a new action, but there are better ways to handle this. We'll discuss that in the next tutorial.

Now you!

The initiator can still only reply with "Fine" when the recipient ask "How about you?". We should change that, so the initiator could also reply with "Could be better". To keep it simple, this will not resort in a conversation.

Keep the structure so that it's still up to the initiator to complete the process. The recipient can only respond with "Sorry to hear that", after which we transition to the state where we wait on the initiator to end the process.

In other words; The initiator can respond with no good, after witch the process transitions to a new state where we expect an acknowledgement from the recipient.

  1. Create a new test file named initiator-not-good.feature

  2. The Before section should go through the process all the way to the state where we wait on the the initiator.

  3. The initiator should then respond with no good, after witch the process transitions to a new state where we expect an acknowledgement from the recipient.

  4. Create a new test Scenario section for this use case.

  5. Modify the scenario so the new test succeeds.

Last updated