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.

YAML
JSON
$schema: "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#"
title: A handshake
actors:
initiator:
title: Initiator
recipient:
title: Recipient
actions:
complete:
actor: initiator
states:
initial:
action: complete
transition: :success
{
"$schema": "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#",
"title": "Basic user",
"actors": {
"intiator": {
"title": "Initiator"
},
"recipient": {
"title": "Recipient"
}
},
"actions": {
"complete": {
"title": "Complete the process",
"actor": "initiator"
}
},
"states": {
"initial": {
"action": "complete",
"transition": ":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.

YAML
JSON
$schema: "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#"
title: A handshake
actors:
initiator:
title: Initiator
recipient:
title: Recipient
actions:
greet:
actor: initiator
reply:
actor: recipient
complete:
actor: initiator
states:
initial:
action: greet
transition: wait_on_recipient
wait_on_recipient:
action: reply
transition: wait_on_initiator
wait_on_initiator:
action: complete
transition: :success
{
"$schema": "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#",
"title": "Basic user",
"actors": {
"intiator": {
"title": "Initiator"
},
"recipient": {
"title": "Recipient"
}
},
"actions": {
"greet": {
"title": "Greet the recipient",
"actor": "initiator"
},
"reply": {
"title": "Reply to the greeting",
"actor": "recipient"
},
"complete": {
"title": "Finish the conversation",
"actor": "initiator"
}
},
"states": {
"initial": {
"action": "greet",
"transition": "wait_on_recipient"
},
"wait_on_recipient": {
"action": "reply",
"transition": "wait_on_initiator"
},
"wait_on_initiator": {
"action": "complete",
"transition": ":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.

YAML
JSON
$schema: "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#"
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:
action: greet
transition: wait_on_recipient
wait_on_recipient:
actions:
- reply
- ignore
transitions:
- action: reply
transition: wait_on_initiator
- action: ignore
transition: :cancelled
wait_on_initiator:
action: complete
transition: :success
{
"$schema": "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#",
"title": "Basic user",
"actors": {
"intiator": {
"title": "Initiator"
},
"recipient": {
"title": "Recipient"
}
},
"actions": {
"greet": {
"actor": "initiator"
},
"reply": {
"actor": "recipient"
},
"ignore": {
"actor": "recipient"
},
"complete": {
"actor": "initiator"
}
},
"states": {
"initial": {
"action": "greet",
"transition": "wait_on_recipient"
},
"wait_on_recipient": {
"actions": [
"reply",
"ignore"
],
"transitions": [
{
"action": "reply",
"transition": "wait_on_initiator"
},
{
"action": "ignore",
"transition": "wait_on_recipient"
}
]
},
"wait_on_initiator": {
"action": "complete",
"transition": ":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 "expect_sympathy" 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.

YAML
JSON
$schema: "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#"
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:
action: greet
transition: wait_on_recipient
wait_on_recipient:
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:
actions:
- sympathize
- complete
transitions:
- action: sympathize
transition: recipient_can_elaborate
- action: complete
transition: :success
recipient_can_elaborate:
action: elaborate
transition: wait_on_initiator
wait_on_initiator:
action: complete
transition: :success
{
"$schema": "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#",
"title": "Basic user",
"actors": {
"intiator": {
"title": "Initiator"
},
"recipient": {
"title": "Recipient"
}
},
"actions": {
"greet": {
"actor": "initiator"
},
"reply": {
"actor": "recipient",
"responses": [
"ok": {},
"not_good": {}
]
},
"ignore": {
"actor": "recipient"
},
"complete": {
"actor": "initiator"
}
},
"states": {
"initial": {
"action": "greet",
"transition": "wait_on_recipient"
},
"wait_on_recipient": {
"actions": [
"reply",
"ignore"
],
"transitions": [
{
"action": "reply",
"response": "ok",
"transition": "wait_on_initiator"
},
{
"action": "reply",
"response": "not_good",
"transition": "expect_sympathy"
},
{
"action": "ignore",
"transition": "wait_on_recipient"
}
]
},
"expect_sympathy": {
"actions": [
"sympathize",
"complete"
],
"transitions": [
{
"action": "sympathize"
"transition": "recipient_can_elaborate"
},
{
"action": "complete"
"transition": ":success"
}
]
},
"recipient_can_elaborate": {
"action": "elaborate",
"transistion": "wait_on_initiator"
},
"wait_on_initiator": {
"action": "complete",
"transition": ":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.

YAML
JSON
recipient_can_elaborate:
action: elaborate
transition: expect_sympathy_from_initiator
"recipient_can_elaborate": {
"action": "elaborate",
"transistion": "expect_sympathy_from_initiator"
},

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.

YAML
$schema: "https://specs.livecontracts.io/v0.2.0/scenario/schema.json#"
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_from_initiator
- 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.